本章主要内容:

  • 使用JavaScript Set数据结构跟踪多个窗口
  • 促进主进程和多个渲染器进程之间的通信
  • 使用Node APIs检查应用程序运行在那个平台上

现在,当Fire Sale启动时,它为UI创建一个窗口。当该窗口关闭时,应用程序退出。虽然这种行为完全可以接受,但我们通常希望能够打开多个独立的窗口。在本章中,我们将Fire Sale从一个单窗口应用程序转换为一个支持多个窗口的应用程序。在此过程中,我们将探索新的Electron APIs以及一些最近添加的JavaScript。我们还将探讨在将一个主进程配置为与一个渲染器进程通信,并对其进行重构以管理可变数量的渲染器进程时出现的问题的解决方案。本章末尾的完整代码可以在http://tinyurl.com/y4z9oj69。 然而我们从第4章-使用本机文件对话框和帮助进程间通讯的分支开始。

图5.1 在第四章中,我们建立了主进程和一个渲染进程之间的通信。

图5.2 在本章中,我们将更新Fire Sale以支持多个窗口并促进他们之间的沟通。

我们首先实例化一个Set数据结构,该结构于2015年添加到JavaScript中,跟踪用户的所有窗口。接下来,我们创建一个函数来管理单个窗口的生命周期。在这之后,我们修改在第4章中创建的函数,以提示用户选择一个文件并打开它以指向正确的窗口。此外,我们还将处理一些常见的突发情况和沿途出现的其他问题,比如互相遮挡的窗口。


创建和管理多个窗口

Sets 是JavaScript的一个新的数据结构,是在ES2015规范中添加的。Set是唯一元素的集合;数组中可以有重复的值。我选择使用set而不是数组,因为这样更容易删除元素。这个清单显示了如何用JavaScript创建一个Set

列表5.1 创建一个跟踪新窗口的集合: ./app/main.js

const windows = new Set();

对于数组,我们要么找到窗口的索引并删除它,要么创建一个没有该窗口的数组。这两种方法都不像调用Set上的delete方法并将引用传递给要删除的窗口那样简单。

有了跟踪应用程序所有窗口的数据结构,下一步是将创建BrowserWindow(列表5.2)从应用程序的"ready"事件监听器移到它自己的函数中。

const createWindow = exports.createWindow = () => {
let newWindow = new BrowserWindow({
show: false,
webPreferences: {
// WebPreferences中的nodeIntegrationInWorker选项设置为true
nodeIntegration: true
}
}); newWindow.loadFile('app/index.html'); newWindow.once('ready-to-show', () => {
newWindow.show();
}); newWindow.on('closed', () => {
windows.delete(newWindow); //从已关闭的窗口Set中移除引用
newWindow = null;
}); windows.add(newWindow); //将窗口添加到已打开时设置的窗口
return newWindow;
};

这个createWindow()函数创建一个BrowserWindow实例并将其添加到我们在清单5.1中创建的一组窗口中。接下来,我们重复前面几章中创建新窗口的步骤。关闭窗口将其从集合中移除,最后,我们返回对刚刚创建的窗口的引用,我们下一章需要这个参考资料。

当应用程序准备好,调用新的createWindow()函数,如下面的清单所示。应用程序应该以与实现此更改之前相同的方式启动,但它也为在其他上下文中创建额外的窗口奠定了基础。

列表5.3 在应用程序就绪时创建窗口: ./app/main.js

app.on('ready',	() => {
createWindow();
});

应用程序像以前一样启动,但是如果您尝试单击Open File按钮,您会注意到它已经坏了。这是因为我们仍然在一些地方引用mainWindow。它在dialog.showOpenDialog()中引用,以在macOS中将对话框显示为工作表。最重要的是,在从文件系统读取文件内容并将其发送到窗口之后,openFile()中引用了它。


主进程和多个窗口之间的通信

拥有多个窗口会引发一个问题:我们将文件路径和内容发送到那个窗口?为了支持多个窗口,这两个函数必须引用应该显示对话框的窗口和发送内容,如图5.3所示。

图5.3 要确定要将文件的内容发送到那个窗口,渲染器进程在与调用getFileFromUser()的主进程通信时必须发送对自身的引用。

在清单5.4中,让我们重构getFileFromUser()函数,以接受一个给定的窗口作为一个参数,而不是总是假设范围中有一个mainWindow实例。

列表5.4 重构getFileFromUser()以处理特定的窗口: ./app/main.js

const getFileFromUser  = exports.getFileFromUser   = (targetWindow) => { //获取对浏览器窗口的引用,以确定应该显示文件对话框的窗口,然后加载用户选择的文件。
const files = dialog.showOpenDialog(targetWindow, { //showopendialog()获取对浏览器窗口对象的引用。
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'Markdown Files', extensions: ['md', 'markdown'] }
]
}); if (files) { openFile(targetWindow, files[0]); } // openFile()函数作用是:获取对浏览器窗口对象的引用,以确定那个窗口应该接受用户打开的文件的内容。
};

在代码清单中,我们修改了getFileFromUser(),将对窗口的引用作为参数。我避免命名参数窗口,因为它可能与浏览器中的全局对象混淆。在用户选择了一个文件之后,除了文件路径之外,我们还将targetWindow传递给openFile(),如下所示。

列表5.5 重构openFile()以处理特定的窗口: ./app/main.js

 const openFile = exports.openFile = (targetWindow, file) => { // 接受对浏览器窗口对象的引用
const content = fs.readFileSync(file).toString();
targetWindow.webContents.send('file-opened', file, content); // 将文件的内容发送到提供的浏览器窗口
};

将对当前窗口的引用传递给主进程

从文件系统读取文件内容之后,我们将文件的路径和内容作为第一个参数传入并发送到窗口。这就提出了一个问题:我们如何获得对窗口的引用。

使用remote模块从渲染器进程调用getFileFromUser(),以便与主进程通信。正如我们在前一章中看到的,remote模块包含对所有模块的引用,否则这些模块只对主进程可用。原来remote还有一些其他方法,尤其是remote还有一些其他方法,尤其是remote.getCurrentWindow(),它返回对调用它的BrowserWindow实例,如下所示。

列表5.6 在渲染器进程中获取对当前窗口的引用: ./app/renderer.js

const currnetWindow = remote.getCurrentWindow();

现在我们有了对窗口的引用,完成该特性的最后一步是将它传递给getFileFromUser()。这让主进程中的函数知道它们正在使用的是什么浏览器窗口。

openFileButton.addEventListener('click', () => {
mainProcess.getFileFromUser(currnetWindow);
});

当我们在第三章中为UI实现Markup时,我们包括了一个New File按钮。我们现在在主进程中实现并导入createWindow()函数,我们也可以很快地把那个按钮连接起来。

列表5.8 向newFileButton添加监听器: ./app/renderer.js

newFileButton.addEventListener('click', ()=> {
mainProcess.createWindow();
})

我们可以在主进程中对多个窗口的实现做一些增强,但是我们已经完成了本章的渲染器进程。下面是app/renderer.js中文件的所有代码。

列表5.9 newFileButton在渲染器进程中的实现: ./app/renderer.js

const { remote, ipcRenderer } = require('electron');
const mainProcess = remote.require('./main.js')
const currnetWindow = remote.getCurrentWindow(); const marked = require('marked'); const markdownView = document.querySelector('#markdown');
const htmlView = document.querySelector('#html');
const newFileButton = document.querySelector('#new-file');
const openFileButton = document.querySelector('#open-file');
const saveMarkdownButton = document.querySelector('#save-markdown');
const revertButton = document.querySelector('#revert');
const saveHtmlButton = document.querySelector('#save-html');
const showFileButton = document.querySelector('#show-file');
const openInDefaultButton = document.querySelector('#open-in-default'); const renderMarkdownToHtml = (markdown) => {
htmlView.innerHTML = marked(markdown, { sanitize: true });
};
markdownView.addEventListener('keyup', (event) => {
const currentContent = event.target.value;
renderMarkdownToHtml(currentContent);
});
newFileButton.addEventListener('click', () => {
mainProcess.createWindow();
});
openFileButton.addEventListener('click', () => {
mainProcess.getFileFromUser(currentWindow);
});
ipcRenderer.on('file-opened', (event, file, content) => {
markdownView.value = content;
renderMarkdownToHtml(content);
});

改进创建新窗口的体验

在实现上一章中的事件监听器之后单击new File按钮,您可能会对它是否正常工作感到困惑。您可能已经注意到窗口周围的阴影变暗了,或者您可能单击并拖动了新窗口,并显示了下面的前一个窗口。

我们现在遇到的一个小问题是,每个新窗口都出现在与第一个窗口相同的默认位置,并且完全遮住了它。更明显的是,如果新窗口与前一个窗口稍微偏移,就会创建新窗口,如图5.4所示。这个清单显示了如何偏移窗口。

清单5.10 基于当前焦点窗口偏移新窗口: ./app/main.js

const createWindow = exports.createWindow = () => {
let x,y; const currentWindow = BrowserWindow.getFocusedWindow(); //获取当前活动的浏览器窗口。 if(currentWindow) { //如果上一步中有活动窗口,则根据当前活动窗口的右下方设置下一个窗口的坐标
const [ currentWindowX, currentWindowY ] = currentWindow.getPosition();
x = currentWindowX + 10;
y = currentWindowY +10;
} let newWindow = new BrowserWindow({
x,
y,
show: false,
webPreferences: {
// WebPreferences中的nodeIntegrationInWorker选项设置为true
nodeIntegration: true
}
}); //创建新窗口,首先使用x和y坐标隐藏它。如果上一步中代码运行了,则设置这些值;如果没有运行,则未定义这些值,在这种情况下,将在默认位置创建窗口。 newWindow.loadFile('app/index.html'); newWindow.once('ready-to-show', () => {
newWindow.show();
}); newWindow.on('closed', () => {
windows.delete(newWindow);
newWindow = null;
}); windows.add(newWindow);
return newWindow;
};

除了使用new关键字实例化实例外,BrowserWindow模块还有自己的方法。我们可以使用BrowserWindow.getFocusedWindow()获得对用户当前正在使用的窗口的引用。当应用程序第一次准备好并调用createWindow()时,没有一个焦点窗口,`BrowserWindow.getFocusedWindow()返回undefined。如果有一个窗口,我们调用它的getWindow()方法,该方法返回一个此窗口的x和y坐标的数组。我们将把这些值存储在条件块之外的两个变量中,并将它们传递给BrowserWindow构造函数。如果它们仍然是未定义的(例如,没有焦点窗口),那么Electron将使用缺省值,就像我们实现此功能之前所做的那样。图5.4显示了与第一个窗口相比的第二个窗口偏移量。

图5.4 新窗口偏移当前窗口

这不是实现此功能的唯一方法。或者,您可以跟踪初始的x和y位置,并在每个新窗口上增加这些值。或者,您可以为默认的x和y值添加一点随机性,这样每个窗口都是稍微偏移量。我把这些方法留给读者作为练习。


结合macOS

在macOS中,即使所有的窗口都关闭了,许多(但不是所有)应用程序仍然保持打开状态。例如,如果您关闭了Chrome中的所有窗口,应用程序在dock中仍然出于活动状态,并且仍然出现在应用程序切换器中。Fire Sale不能做到这点。

在前几张章中,这可能是可以接受的。我们只有一个窗口,无法创建其他窗口。在本节中,我们只允许应用程序在macOS中保持打开状态。默认情况下,当Electron触发它的window-all-closed事件时,它将退出应用程序。如果我们想要阻止这种行为,我们必须监听这个事件,并且在macOS上运行时有条件地阻止它关闭。

列表5.11 在关闭所有窗口时保持应用程序的活动状态: ./app/main.js

app.on('window-all-closed', () => {
if(process.platform === 'darwin') { //检查应用程序是否在macOS上运行
return false; //如果是,则返回false以防止默认操作
}
app.quit(); //如果不是,则退出应用程序
});

process对象由Node提供,不需要配置全局可用。process.platform返回当前执行应用程序的平台名称。在截至写作时间点,process.platform返回七个字符串之一: aix,darwin,freebsd,linux,openbsd,sunoswin32。Darwin是构建macOS的UNIX操作系统。在清单5.11中,我们检查了是否process.platform等于darwin,如果是,则应用程序正在macOS上运行,我们希望返回false以阻止默认操作的发生。

保持应用程序的活动是成功的一半,如果用户单击dock中的应用程序而没有打开窗口,会发生什么?在这种情况下,Fire Sale应该打开一个新窗口并显示给用户,如下所示。

图5.12 在应用程序打开时创建一个窗口,但没有窗口: ./app/main.js

app.on('activate', (event, hasVisibleWindows) => { //Electron提供了hasVisibleWindows参数,它将是一个布尔值。
if(!hasVisibleWindows) { createWindow(); } //如果用户激活应用程序时没有可见窗口,则创建一个。
});

activate事件将两个参数传递给提供的回调函数。第一个是event对象,第二个是布尔值,如果任何窗口都可见,则返回true;如果所有窗口都关闭,则返回false.对于后者,我们调用本章前面编写的createWindow()函数。

activate事件只在macOS上触发,但是有很多原因可以解释为什么您可能选择让您的应用程序在Windows或Linux上保持打开状态,特别是如果应用程序正在运行后台进程,而您希望继续运行这些进程,即使该窗口被关闭。另一种可能性是,您的应用程序可以隐藏,或者使用全局快捷方式显示,或者从托盘或菜单栏中显示。我们将在后面的章节中实现这些。

通过这两个额外的事件,我们将Fire Sale从单窗口应用程序转换为支持多窗口的应用。这个清单显示了主进程当前状态的代码。

列表5.13 在主进程中实现多个窗口: ./app/main.js

const{ app, BrowserWindow,dialog } = require('electron');
const fs = require('fs'); const windows = new Set(); app.on('ready', () => {
createWindow();
}); app.on('window-all-closed', () => {
if(process.platform === 'darwin') {
return false;
}
}); app.on('activate', (event, hasVisibleWindows) => {
if(!hasVisibleWindows) { createWindow(); }
}); const createWindow = exports.createWindow = () => {
let x,y; const currentWindow = BrowserWindow.getFocusedWindow(); if(currentWindow) {
const [ currentWindowX, currentWindowY ] = currentWindow.getPosition();
x = currentWindowX + 10;
y = currentWindowY +10;
} let newWindow = new BrowserWindow({
x,
y,
show: false,
webPreferences: {
// WebPreferences中的nodeIntegrationInWorker选项设置为true
nodeIntegration: true
}
}); newWindow.loadFile('app/index.html'); newWindow.once('ready-to-show', () => {
newWindow.show();
}); newWindow.on('closed', () => {
windows.delete(newWindow);
newWindow = null;
}); windows.add(newWindow);
return newWindow;
}; const getFileFromUser = exports.getFileFromUser = (targetWindow) => {
const files = dialog.showOpenDialog(targetWindow, {
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'Markdown Files', extensions: ['md', 'markdown'] }
]
}); if (files) { openFile(targetWindow, files[0]); } // A
}; const openFile = (targetWindow, file) => {
const content = fs.readFileSync(file).toString();
targetWindow.webContents.send('file-opened', file, content); // B
};

总结

  • 当创建具有多个窗口的Electron应用程序时,我们不能硬编码主进程发送数据的窗口。
  • 我们可以使用Electron的remote模块向渲染器进程中的窗口请求对自身的引用,并在与主进程通信时发送该引用。
  • macOS上的应用程序并不总是在所有窗口都关闭时退出,我们可以使用Node的process对象来确定应用程序在那个平台上运行。
  • 如果process.platformdarwin,则应用程序在macOS上运行。
  • 在监听应用程序的windows-all-closed事件的函数中,返回false从而防止应用程序退出。
  • 在macOS上,当用户单击dock图标时,应用程序会触发activate事件。
  • activate事件包含一个名为hasVisibleWindows的布尔值,作为传递给回调函数的第二个参数。

    如果当前有窗口打开,则为true;如果没有窗口,则为false。我们可以用它来决定是否应该打开一个新窗口。

第五章-处理多窗口 | Electron实战的更多相关文章

  1. RxJava2实战---第五章 变换操作符和过滤操作符

    RxJava2实战---第五章 变换操作符和过滤操作符 RxJava的变换操作符主要包括以下几种: map():对序列的每一项都用一个函数来变换Observable发射的数据序列. flatMap() ...

  2. 《Spring实战》学习笔记-第五章:构建Spring web应用

    之前一直在看<Spring实战>第三版,看到第五章时发现很多东西已经过时被废弃了,于是现在开始读<Spring实战>第四版了,章节安排与之前不同了,里面应用的应该是最新的技术. ...

  3. Spring实战第五章学习笔记————构建Spring Web应用程序

    Spring实战第五章学习笔记----构建Spring Web应用程序 Spring MVC基于模型-视图-控制器(Model-View-Controller)模式实现,它能够构建像Spring框架那 ...

  4. 2017.2.28 activiti实战--第五章--用户与组及部署管理(三)部署流程及资源读取

    学习资料:<Activiti实战> 第五章 用户与组及部署管理(三)部署流程及资源读取 内容概览:如何利用API读取已经部署的资源,比如读取流程定义的XML文件,或流程对应的图片文件. 以 ...

  5. 2017.2.28 activiti实战--第五章--用户与组及部署管理(二)部署流程资源

    学习资料:<Activiti实战> 第五章 用户与组及部署管理(二)部署流程资源 内容概览:讲解流程资源的读取与部署. 5.2 部署流程资源 5.2.1 流程资源 流程资源常用的有以下几种 ...

  6. 2017.2.20 activiti实战--第五章--用户与组及部署管理(一)用户与组

    学习资料:<Activiti实战> 第五章 用户与组及部署管理(一)用户与组 内容概览:讲解activiti中内置的一套用户.组的关系,以及如何通过API添加.删除.查询. 5.1 用户与 ...

  7. 【全面解禁!真正的Expression Blend实战开发技巧】第五章 从最常用ButtonStyle开始 - ImageButton

    原文:[全面解禁!真正的Expression Blend实战开发技巧]第五章 从最常用ButtonStyle开始 - ImageButton 本章围绕ImageButton深入讨论,为什么是Image ...

  8. 第四章-使用本机文件对话框和帮助进程间沟通 | Electron实战

    本章主要内容: 使用Electron的dialog模块实现一个本机打开文件对话框 促进主进程和渲染器进程之间的通信 将功能从主进程暴露给渲染器进程 使用Electron的remote模块从主进程导入功 ...

  9. [书籍翻译] 《JavaScript并发编程》第五章 使用Web Workers

    本文是我翻译<JavaScript Concurrency>书籍的第五章 使用Web Workers,该书主要以Promises.Generator.Web workers等技术来讲解Ja ...

随机推荐

  1. HDU 1565:方格取数(1)(最大点权独立集)***

    http://acm.hdu.edu.cn/showproblem.php?pid=1565 题意:中文. 思路:一个棋盘,要使得相邻的点不能同时选,问最大和是多少,这个问题就是最大点权独立集. 可以 ...

  2. 双端队列 duque

    一.双端队列(Deque) - 概念:deque(也称为双端队列)是与队列类似的项的有序集合.它有两个端部,首部和尾部,并且项在集合中保持不变. - 特性:deque 特殊之处在于添加和删除项是非限制 ...

  3. Linux 安装 lanmp

    Lanmp介绍 lanmp一键安装包是wdlinux官网2010年底开始推出的web应用环境的快速简易安装包. 执行一个脚本,整个环境就安装完成就可使用,快速,方便易用,安全稳定 lanmp一键安装包 ...

  4. NetCore 中间件获取请求报文和返回报文

    using System; using System.IO; namespace WebApi.Restful.Middlewares { public class MemoryWrappedHttp ...

  5. ~~Py2&Py3~~

    进击のpython python2 整型 int -- long(长整型) /获取的是整数 python3 整型 int /获取的是浮点数(小数) python2 print(range(1,10)) ...

  6. 原创:Python编写通讯录,支持模糊查询,利用数据库存储

    1.要求 数据库存储通讯录,要求按姓名/电话号码查询,查询条件只有一个输入入口,自动识别输入的是姓名还是号码,允许模糊查询. 2.实现功能 可通过输入指令进行操作. (1)首先输入“add”,可以对通 ...

  7. Jmeter自定义Java请求开发

    一.本次实验目的 IDEA新建maven项目,使用java开发自定义jmeter的请求. 本次开发使用的代码,会百度云分享给大家. 二.本次实验环境 Idea 2017.02 Jmeter 5.1.1 ...

  8. nu.xom:Serializer

    Serializer: 机翻 /* 使用用于控制空格,规范化,缩进,换行和基本URI的各种选项以特定编码输出Document对象 */ Serializer(OutputStream out) :创建 ...

  9. 【基础算法-模拟-例题-玩具谜题】-C++

    原题链接P1563 玩具谜题 这道题依然是一道模拟题目,就简单讲讲坑点: 1.有些时候在转圈的时候要用到它们站成了一个环的性质! 2.小人面朝的方向影响了它们左右的方向! 3.注意考虑顺时针逆时针与小 ...

  10. [USACO09OCT]Invasion of the Milkweed】乳草的侵占-C++

    Farmer John一直努力让他的草地充满鲜美多汁的而又健康的牧草.可惜天不从人愿,他在植物大战人类中败下阵来.邪恶的乳草已经在他的农场的西北部份占领了一片立足之地. 草地像往常一样,被分割成一个高 ...