const getFileFromUser = exports.getFileFromUser = () => {
const files = dialog.showOpenDialog(mainWindow, { // 传递对BrowserWindow实例的引用对话框。showOpenDialog将导致macOS将对话框显示为从窗口标题栏向下的工作表。它对Windows和Linux没有影响。
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'Markdown Files', extensions: ['md', 'markdown'] }
if (files) { return; }
const file = files[0];
const content = fs.readFileSync(file).toString();
console.log(content);
通过这个简单的更改,Electron现在将Open File对话框显示为一个工作表,该工作表从传递给方法的窗口下拉,如图4.8所示。
图4.8 在macOS中,打开文件对话框现在从菜单的标题栏下拉,而不是作为应用程序窗口前面的附加窗口出现。
促进进程间通信
我们已经编写了用于在主进程中选择和读取文件的所有代码。但是我们如何将文件的内容发送到渲染器进程呢?如何从UI中触发主进程中的getFileFromUser()函数?
在构建传统web应用程序时,我们必须处理类似的问题。这并不完全相同,因为所有的代码都在客户机的计算机上运行,但是考虑一下我们通常如何构建web应用程序,可以作为理解如何构造Electron应用程序的一个有用的比喻。 参见图4.9。
图4.9 Electron与传统web应用程序的职责划分
在web上,我们通常在以下两个地方编写代码: 在服务器上或在用户浏览器中运行的客户端代码。客户端代码呈现UI,它监听并处理用户操作,并更新UI以显示应用程序的当前状态。然而,我们对客户端代码所能做的事件是有限制的。正如我们在第一章中讨论的,我们不能读取数据库或文件系统。服务端代码在我们的计算机上运行,它可以访问数据库,它可以写入我们系统上的日志文件。
在传统的web应用程序中,我们通常使用HTTP之类的协议来促进客户机和服务端进程之间的通信。使用HTTP,客户机可以发送带有信息的请求,服务器接受此请求,适当地处理它,并向客户机发送响应。
在Electron应用程序中,情况有些不同。正如我们在前几章中讨论过的,Electron应用由多个进程组成: 一个主进程和一个或多个渲染进程。所有东西都在我们的计算机上运行,但是角色的分离与客户机-服务器模型类似。我们不使用HTTP在进程之间通信。相反,Electron提供了几个模块来协调主进程和渲染进程之间的通信。
我们的主进程负责与本机操作系统APIs进行连接,它负责生成渲染器进程、定义应用程序菜单和显示打开和保存对话框、注册全局快捷方式、从操作系统请求电源信息、以及更多。执行这些任务所需的模块在Electron仅在主进程中可用来实现这一点,如图4.10所示。
图4.10 Electron提供不同的模块给主进程和渲染进程。这些模块代表了Electron的代码功能,到您阅读本文时,这个列表可能还会增长,并且可能还不完整。我鼓励您访问文档以查看最新的和最棒的特性。
Electron只向每个进程提供其模块的一个子集,而不保留我们访问与Electron模块分离的Node的APIs。如果愿意,我们可以从渲染器进程访问数据库和文件系统,但是有一些令人信服的理由将这种功能保留在主进程中。我们可能有很多渲染器进程,但是我们总是只有一个主进程。从我们的众多的渲染器读取和写入文件系统可能会出现问题;一个或多个进程试图同时写入同一个文件,或者从一个文件中读取,而另一个渲染器进程正在重写该文件。
JavaScript中的一个给定进程在一个线程上执行我们的代码,并且一次只能做一件事。通过将这些任务委托给主进程,我们可以确信一次只有一个进程执行对给定文件或数据库的读写。其他任务遵循正常的JavaScript协议,在事件队列中耐心等待,直到主进程完成当前任务。
主进程处理调用本机操作系统APIs或提供文件系统访问的任务是有意义的,但是触发这些操作的UI在渲染器进程中调用。即使所有的代码都在同一台计算机上运行,我们仍然需要协调进程之间的通信,因为我们必须协调客户机和服务器之间的通信。
最近,出现了WebSockets和WebRTC等协议,它们允许客户机和服务器之间的双向通信,甚至客户机之间的通信,而不需要中央服务器来促进通信。当我们构建桌面应用程序时,我们通常不会使用HTTP或WebSockets,但是Electron有几种协调进程间通信的方法,我们将在本章开始探讨,如图4.11所示。
图4.11 实现打开文件按钮涉及协调渲染器进程和主进程。
我们的UI包含一个带有标签Open File的按钮。当用户单击此按钮时,我们的应用程序应该提供一个对话框,允许用户选择要打开的文件。在用户选择一个文件之后,我们的应用程序应该读取文件的内容,在应用程序的左窗格中显示它们,并在右窗格中呈现相应的HTML。
正如您可能已经猜到的,这需要我们在两者之间进行协调渲染器进程(单击按钮的地方)和主进程(负责显示对话框并从文件系统中读取所选文件)。读取文件之后,主进程需要将文件的内容发送回渲染器进程(下一个清单),以便分别在左窗格和右窗格中显示和呈现。
列表4.7 在渲染器进程中添加事件监听器
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);
openFileButton.addEventListener('click', () => { //选择一个更新的CSS框模型,它将正确地设置元素的宽度和高度
alert('You clicked the "Open File" button.');
首先将事件监听器添加到渲染器进程中的Open File按钮。有了事件监听器,就可以与主进程协调,触发前面创建的Open File对话框。
介绍remote模块
Electron提供了许多方便进程间通信的方法。第一个是remote模块-一种从渲染器进程到主进程执行进程间通信的简单方法。
remote模块(仅在呈现器进程中可用)通过镜像主进程中可访问的模块,充当主进程的代理。当我们访问任何这些属性时,远程模块还负责与主进程之间的通信。
如图4.12所示,remote模块有几个属性,这些属性与仅对主进程可用的模块重叠。在我们的渲染器进程中,我们可以引用remote模块,它提供了对主进程中的对象和属性的访问,如图4.13所示。
图4.12 remote模块提供对通常仅对主进程可用的模块的访问。
图4.13 remote模块提供对通常仅对主进程可用的模块的访问。
当我们调用remote对象上的方法或属性时,它向主进程发送同步消息,在主进程中执行,并将结果发送回渲染器进程。remote模块允许我们在主进程中定义功能,并且很容易使其对渲染器进程可用。
使用进程间通信触发Open File函数
应用程序现在可以触发“Open File”对话框并读取用户在主进程中选择的文件。我们还向进程中的Open File按钮添加了一个事件监听器。现在只需要使用我们前面讨论过的进程间通信技术将它们连接起来。
理解CommonJS引用系统
通过remote模块使用主进程的功能,我们需要利用Node的CommonJS模块系统向应用程序中的其他文件公开该功能。在本书中,我们使用了require从Electron,Node标准库和第三方库中提取功能,但这是我们第一次将其与我们的代码一起使用。让我们花几分钟回顾一下它是如何工作的。
Node的模块系统由2个主要的方法所组成:从其他来源获取功能的能力,以及导出功能供其他来源使用的能力。当我们需要来自其他资源的代码时,其他资源可以是我们编写的文件、一个第三方模块、一个Node模块或Electron提供的模块。我们在主进程和渲染进程的顶部都使用了Node的内置requrie函数
当我们需要一个模块时,我们究竟要导入什么?在Node中,我们显式地声明应该从模块导出什么功能,如清单4.8所示。这个函数在清单4.9中导入,Node中的每个模块都有一个名为exports的内置对象,它从一个空对象开始。当我们从另一个文件中需要导出对象时,添加到导出对象的任何内容都是可用的。
清单4.8 在Node导出一个函数: basic-math.js
exports.addTwo = n => n + 2;
从另一个进程引用功能
内置的require函数不能跨进程工作。当我们在渲染器进程中工作时,我们使用内置的require函数导入的任何功能都将是渲染器进程的一部分。当我们在主进程中工作时,我们需要的任何功能都将是主进程的一部分。但是当我们在渲染器进程中想要从主进程中获得功能时,会发生什么呢?
Electron的remote模块有它自己的require方法,在我们的渲染器进程中允许它从主进程获取功能。使用remote.require返回代理对象—类似于远程对象上的其他属性。Electron代表我们负责所有的进程间通信。
要实现本章开头所述的功能,主进程必须导出它的getFileFromUser()函数,以便我们可以将它导入到渲染器进程代码中。这个清单更新了app/main.js中的一行。
清单4.10 从渲染器进程中导出打开文件对话框的功能: ./app/main.js
const getFileFromUser = exports.getFileFromUser = () => { //除了在这个文件中创建一个常量外,我们还将它指定为exports对象的一个属性,该属性可以从其他文件(特别是渲染器进程)访问。
const files = dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'Markdown Files', extensions: ['md', 'markdown'] }
if (files) { returun; }
const file = files[0];
const content = fs.readFileSync(file).toString();
console.log(content);
代码接受我们创建的getFileFromUser()函数,并将其导出为exports对象上具有相同名称的属性。渲染进程需要引入Electron的 remote 模块,然后使用remote.require。从渲染器进程的主进程获取对getFileFromUser()函数的引用。这与清单4.11中内置的require函数不同,因为导入的代码是根据主进程计算的,而不是根据引入它的渲染器进程计算的。这需要四个步骤:
在渲染器进程中需要Electron。
存储对remote的引用。
使用remote.require请求主进程。
存储从主进程导出的getFileFromUser()函数的应用。
列表4.11 渲染器进程中需要主进程的功能: ./app/renderer.js
const { remote } = require('electron');
const mainProcess = remote.require('./main.js');
现在,我们可以在渲染器进程中调用从主进程导出getFileFromUser()函数。让我们替换事件监听器中的功能,以触发Open File对话框,而不是触发警报。
列表4.12 从UI触发主进程中的getFileFromUser(): ./app/ renderer.js
openFileButton.addEventListener('click', () => {
mainProcess.getFileFromUser();
如果我们启动Electron应用程序并单击“Open File”按钮,它将正确地触发“打开文件”对话框。有了这些,我们仍然只将文件记录到主进程中的控制台。为了完成我们的特性,主进程必须将文件的内容发送回呈现器进程,以便在我们的应用程序中显示。
将内容从主进程发送到渲染器进程
remote模块促进了渲染器进程访问主进程的能力,但是它不允许主进程访问渲染器进程。要将用户选择的文件内容发送回要在UI中呈现的渲染器进程的话,我们需要学习进程之间通信的另一种技术。
每个BrowserWindow实例都有一个名为webContents的属性,它存储一个对象,该对象负责在调用new BrowserWindow()时创建的web浏览器窗口。webContents与app类似,因为它在渲染器进程中根据web页面的生命周期发出事件。
以下是一些不完整的事件列表,你可以在webContents对象上监听:
did-start-loading
did-stop-loading
dom-ready
focus
resize
enter-full-screen
leave-full-screen
webContents还有许多方法,可以在渲染器进程中触发与主进程不同的函数。在前一章中,我们通过主进程使用mainWindow.webContents.openDevTools()在渲染器进程中打开了Chrome开发工具。mainWindow.loadURL('file://${__dirname}/ index.html')是mainWindow.webContents.loadURL()的别名,它在应用程序首次启动时将HTML文件加载到渲染器进程中。图4.14显示了更多的别名。
图4.14 BrowserWindow实例的方法是Electron webContents API的别名。
webContents有一个名为send()的方法,它将信息从主进程发送到渲染器进程。webContents.send()接受可变数量的参数。第一个参数是用来发送消息的通道的名称,它是一个任意字符串。渲染器进程中的事件监听器在同一通道上监听。当我们看到它的行动时,这种流动将变得更加清晰。第一个参数之后的所有后续参数都传递给渲染器进程。
发送文件内容到渲染器进程
我们当前实现是读取用户选择的文件并打印到终端上,mainWindow.webContents.send()将文件的内容发送到渲染器进程中。下一章将介绍打开文件的其他方法,这些方法不需要一个对话框来提示用户选择特定的文件,因为我们确实会遇到一些情况,在不触发对话框的情况下打开文件。
列表4.13 从主进程发送内容到渲染器进程: ./app/main.js
const getFileFromUser = exports.getFileFromUser = () => {
const files = dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'Markdown Files', extensions: ['md', 'markdown'] }
if (files) { openFile(files[0]); } // 在前面,在文件未定义的情况下,使用return语句中断了函数。在本例中,当dialog.showOpenFile()成功返回一个文件路径数组时,我们将调整逻辑并将第一个文件传递给Open File。
const openFile = (file) => {
const content = fs.readFileSync(file).toString();
mainWindow.webContents.send('file-opened', file, content); // 我们将通过"file-opened"通道将文件的名称及其内容发送到渲染器进程
主进程现在通过打开的文件file-opened通道广播文件的名称及其内容。下一步是使用ipcRenderer模块在渲染器进程中file-opened通道上设置监听器。Electron提供了两个基本模块,用于在进程之间来回发送消息: ipcRenderer和ipcMain。每个模块仅在与之共享名称的进程类型中可用。
ipcRender可以向主进程发送消息,最重要的是,它还可以监听使用webContents.send()从主进程发送的消息。它在渲染器进程中需要ipcRenderer模块。
列表4.14 导入ipcRenderer模块: ./app/renderer.js
const { remote, ipcRenderer } = require('electron'); //将在我们的渲染器进程中导入ipcRenderer模块
const mainProcess = remote.require('./main.js')
有了这些,我们现在可以设置一个监听器。ipcRenderer监听file-opened通道,将内容添加到页面,并将Markdown渲染为HTML。
列表4.15 在file-opened通道上监听消息
ipcRenderer.on('file-opened', (event, file, content) => {
markdownView.value = content;
renderMarkdownToHtml(content);
ipcRenderer.on接受两个参数:要监听的参数和一个回调函数,回调函数定义当渲染器进程在设置监听器的通道上接受到消息时要采取的操作。回调函数在调用时提供几个参数,第一个是事件对象,它与浏览器中的普通事件监听器一样。它包含关于我们为其设置监听器事件的消息,其他参数是在主进程中使用webContents.send()时提供的。在清单4.13中,我们发送了文件的名称及其内容,这些将是传递给监听器的附加参数。
有了这些新增功能,用户现在可以单击Open File按钮,使用本机文件对话框选择一个文件,并在UI中呈现内容。我们已经成功地实现了我们在本章开始时设定的特性,我们的主进程和渲染进程的代码应该类似于以下两个清单。
列表4.16 在主进程实现打开文件的功能: ./app/main.js
const{ app, BrowserWindow,dialog } = require('electron');
const fs = require('fs');
let mainWindow = null;
app.on('ready', () => {
mainWindow = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true
mainWindow.loadFile('app/index.html');
mainWindow.once('ready-to-show', () => {
mainWindow.show();
mainWindow.on('closed', () => {
mainWindow = null;
const getFileFromUser = exports.getFileFromUser = () => {
const files = dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'Markdown Files', extensions: ['md', 'markdown'] }
if (files) { openFile(files[0]); }
const openFile = (file) => {
const content = fs.readFileSync(file).toString();
mainWindow.webContents.send('file-opened', file, content);
const { remote, ipcRenderer } = require('electron');
const mainProcess = remote.require('./main.js')
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);
openFileButton.addEventListener('click', () => {
mainProcess.getFileFromUser();
ipcRenderer.on('file-opened', (event, file, content) => {
markdownView.value = content;
renderMarkdownToHtml(content);
打开对话框可以配置为只允许用户选择特定的文件类型。
打开对话框返回一个数组,该数组由用户选择的一个或多个文件或目录组成。
Electron不包括读取文件的能力,相反,我们使用Node的fs模块来读写文件系统。
每个操作系统都提供了一组不同的功能。如果在给定的操作系统中不存在该特性,那么Electron将使用可用的特性,同时提供一个优雅的后备。
在macOS中,我们可以通过在dialog. showopendialog()中提供对该窗口的引用作为第一个参数,使对话框从其中一个窗口作为工作表下拉。
本机操作系统APIs和文件系统访问应该由主进程处理,而呈现UI和响应用户输入应该由渲染器进程处理。
Electron提供了一套不同的模块给主进程和渲染器进程。
remote模块为主进程模块和函数提供代理,并使该功能在渲染器进程中可用。
我们可以使用webContents.send ()命令将消息从主进程发送到渲染器进程。
我们可以使用ipcRenderer模块监听主进程发送渲染器进程的消息。
我们可以使用通道来命名消息的名称空间,通道是任意字符串。在本章中,我们使用file-opened的通道发送和侦听消息。