第五章-处理多窗口 | Electron实战
本章主要内容:
- 使用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
,sunos
或win32
。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.platform
是darwin
,则应用程序在macOS上运行。 - 在监听应用程序的
windows-all-closed
事件的函数中,返回false从而防止应用程序退出。 - 在macOS上,当用户单击dock图标时,应用程序会触发
activate
事件。 activate
事件包含一个名为hasVisibleWindows
的布尔值,作为传递给回调函数的第二个参数。
如果当前有窗口打开,则为true
;如果没有窗口,则为false
。我们可以用它来决定是否应该打开一个新窗口。
第五章-处理多窗口 | Electron实战的更多相关文章
- RxJava2实战---第五章 变换操作符和过滤操作符
RxJava2实战---第五章 变换操作符和过滤操作符 RxJava的变换操作符主要包括以下几种: map():对序列的每一项都用一个函数来变换Observable发射的数据序列. flatMap() ...
- 《Spring实战》学习笔记-第五章:构建Spring web应用
之前一直在看<Spring实战>第三版,看到第五章时发现很多东西已经过时被废弃了,于是现在开始读<Spring实战>第四版了,章节安排与之前不同了,里面应用的应该是最新的技术. ...
- Spring实战第五章学习笔记————构建Spring Web应用程序
Spring实战第五章学习笔记----构建Spring Web应用程序 Spring MVC基于模型-视图-控制器(Model-View-Controller)模式实现,它能够构建像Spring框架那 ...
- 2017.2.28 activiti实战--第五章--用户与组及部署管理(三)部署流程及资源读取
学习资料:<Activiti实战> 第五章 用户与组及部署管理(三)部署流程及资源读取 内容概览:如何利用API读取已经部署的资源,比如读取流程定义的XML文件,或流程对应的图片文件. 以 ...
- 2017.2.28 activiti实战--第五章--用户与组及部署管理(二)部署流程资源
学习资料:<Activiti实战> 第五章 用户与组及部署管理(二)部署流程资源 内容概览:讲解流程资源的读取与部署. 5.2 部署流程资源 5.2.1 流程资源 流程资源常用的有以下几种 ...
- 2017.2.20 activiti实战--第五章--用户与组及部署管理(一)用户与组
学习资料:<Activiti实战> 第五章 用户与组及部署管理(一)用户与组 内容概览:讲解activiti中内置的一套用户.组的关系,以及如何通过API添加.删除.查询. 5.1 用户与 ...
- 【全面解禁!真正的Expression Blend实战开发技巧】第五章 从最常用ButtonStyle开始 - ImageButton
原文:[全面解禁!真正的Expression Blend实战开发技巧]第五章 从最常用ButtonStyle开始 - ImageButton 本章围绕ImageButton深入讨论,为什么是Image ...
- 第四章-使用本机文件对话框和帮助进程间沟通 | Electron实战
本章主要内容: 使用Electron的dialog模块实现一个本机打开文件对话框 促进主进程和渲染器进程之间的通信 将功能从主进程暴露给渲染器进程 使用Electron的remote模块从主进程导入功 ...
- [书籍翻译] 《JavaScript并发编程》第五章 使用Web Workers
本文是我翻译<JavaScript Concurrency>书籍的第五章 使用Web Workers,该书主要以Promises.Generator.Web workers等技术来讲解Ja ...
随机推荐
- Codeforces 758D:Ability To Convert(思维+模拟)
http://codeforces.com/problemset/problem/758/D 题意:给出一个进制数n,还有一个数k表示在n进制下的值,求将这个数转为十进制最小可以是多少. 思路:模拟着 ...
- 性能测试即服务-docker部署jmeter及.netcore应用
前言 现在各种业务都追求上云,通俗的讲,“XX即服务”,作为一名专职的性能测试调优人员的我,由于会点三脚猫的开发功夫,“性能测试即服务”这种开发大任就落到我头上了,先做一个能完成核心压测功能的基础版. ...
- CPU缓存和内存屏障
CPU性能优化手段 - 缓存 为了提高程序的运行性能, 现代CPU在很多方面对程序进行了优化例如: CPU高速缓存, 尽可能的避免处理器访问主内存的时间开销, 处理器大多会利用缓存以提高性能 多级缓存 ...
- web文件下载(附方案及源码配置)
1. 场景描述 因项目需查询数据量比较大(需要查询Hbase等nosql数据库),采用用户点击查询后,后台查询并生成查询文件:然后消息通知用户后,用户点击下载的方式来满足用户需求. 2. 解决方案 W ...
- MyBatis从入门到精通:第二章数据的创建与插入文件
数据库表的创建: create table sys_user ( id bigint not null auto_increment, ), user_password ), user_email ) ...
- 从0系统学Android--1.2 手把手带你搭建开发环境
要想进行程序开发,首先我们需要搭建开发环境,下面就开始搭建环境. 1.2.1 所需的工具 首先 Android 开发是基于 Java 的,因此你需要掌握简单的 Java 语法.会基础的 Java 语法 ...
- 8086 IO读写操作
如图所示,通过8086来读写io口,实现流水灯以及开关.本电路是基于8086最小模式下的三总线结构添加的,三总线结构原理较为复杂本篇就不对其原理进行介绍了,大家可以自行查阅相关引脚的功能从而实现. 本 ...
- 洛谷P2285 【[HNOI2004]打鼹鼠】
每次打鼹鼠的机器人总是从某一次打鼹鼠的地方走过来的 对鼹鼠出现时间从小到大排序 f[i]表示到第i个鼹鼠(打第i个)最多能打多少个鼹鼠 f[i]=max(f[j]+1)f[i]=max(f[j]+1) ...
- #!/usr/bin/env bash和#!/usr/bin/bash的比较
#!/usr/bin/env bash和#!/usr/bin/bash的比较 stackoverflow: http://stackoverflow.com/questions/16365130/th ...
- css关于flex布局下不能实现text-overflow: ellipsis的解决办法
摘录自 https://segmentfault.com/q/1010000011115918