现在,当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: {
        
        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;
复制代码
  
   这个
   
    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]); } 
复制代码
  
   在代码清单中,我们修改了
   
    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({
      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
复制代码
除了使用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') { 
    return 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) => { 
    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({
      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])
  const openFile = (targetWindow, file) => {
    const content = fs.readFileSync(file).toString()
    targetWindow.webContents.send('file-opened', file, content)
当创建具有多个窗口的Electron应用程序时,我们不能硬编码主进程发送数据的窗口。
我们可以使用Electron的remote模块向渲染器进程中的窗口请求对自身的引用,并在与主进程通信时发送该引用。
macOS上的应用程序并不总是在所有窗口都关闭时退出,我们可以使用Node的process对象来确定应用程序在那个平台上运行。
如果process.platform是darwin,则应用程序在macOS上运行。
在监听应用程序的windows-all-closed事件的函数中,返回false从而防止应用程序退出。
在macOS上,当用户单击dock图标时,应用程序会触发activate事件。
activate事件包含一个名为hasVisibleWindows的布尔值,作为传递给回调函数的第二个参数。
如果当前有窗口打开,则为true;如果没有窗口,则为false。我们可以用它来决定是否应该打开一个新窗口。
        Electron
                     -  4757
  -  
        SecondaryMarquis
        Electron