流程模型

Electron 继承了来自 Chromium 的多进程架构,这使得此框架在架构上非常相似于一个现代的网页浏览器。 本指南将对 教程 中应用的概念进行拓展。

为什么不是一个单一的进程?

网页浏览器是个极其复杂的应用程序。 除了显示网页内容的主要能力之外,他们还有许多次要的职责,例如:管理众多窗口 ( 或 标签页 ) 和加载第三方扩展。

在早期,浏览器通常使用单个进程来处理所有这些功能。 虽然这种模式意味着您打开每个标签页的开销较少,但也同时意味着一个网站的崩溃或无响应会影响到整个浏览器。

多进程模型

为了解决这个问题,Chrome 团队决定让每个标签页在自己的进程中渲染, 从而限制了一个网页上的有误或恶意代码可能导致的对整个应用程序造成的伤害。 然后用单个浏览器进程控制这些标签页进程,以及整个应用程序的生命周期。 下方来自 Chrome 漫画 的图表可视化了此模型:

Chrome的多进程架构

Electron 应用程序的结构非常相似。 作为应用开发者,你将控制两种类型的进程:主进程渲染器进程。 这类似于上文所述的 Chrome 的浏览器和渲染器进程。

主进程

每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。

窗口管理

主进程的主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口。

BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 您可从主进程用 window 的 webContent 对象与网页内容进行交互。

main.js

  1. const { BrowserWindow } = require('electron')
  2. const win = new BrowserWindow({ width: 800, height: 1500 })
  3. win.loadURL('https://github.com')
  4. const contents = win.webContents
  5. console.log(contents)

注意:渲染器进程也是为 web embeds 而被创建的,例如 BrowserView 模块。 嵌入式网页内容也可访问 webContents 对象。

由于 BrowserWindow 模块是一个 EventEmitter, 所以您也可以为各种用户事件 ( 例如,最小化 或 最大化您的窗口 ) 添加处理程序。

当一个 BrowserWindow 实例被销毁时,与其相应的渲染器进程也会被终止。

应用程序生命周期

主进程还能通过 Electron 的 app 模块来控制您应用程序的生命周期。 该模块提供了一整套的事件和方法,可以让您用来添加自定义的应用程序行为 (例如:以编程方式退出您的应用程序、修改应用程序坞,或显示一个关于面板) 。

这是一个实际的例子,这个app来源于快速入门指南,用 app API 创建了一个更原生的应用程序窗口体验。

main.js

  1. // quitting the app when no windows are open on non-macOS platforms
  2. app.on('window-all-closed', () => {
  3. if (process.platform !== 'darwin') app.quit()
  4. })

原生 API

为了使 Electron 的功能不仅仅限于对网页内容的封装,主进程也添加了自定义的 API 来与用户的作业系统进行交互。 Electron 有着多种控制原生桌面功能的模块,例如菜单、对话框以及托盘图标。

关于 Electron 主进程模块的完整列表,请参阅我们的 API 文档。

渲染器进程

每个 Electron 应用都会为每个打开的 BrowserWindow ( 与每个网页嵌入 ) 生成一个单独的渲染器进程。 洽如其名,渲染器负责 渲染 网页内容。 所以实际上,运行于渲染器进程中的代码是须遵照网页标准的 (至少就目前使用的 Chromium 而言是如此) 。

因此,一个浏览器窗口中的所有的用户界面和应用功能,都应与您在网页开发上使用相同的工具和规范来进行攥写。

虽然解释每一个网页规范超出了本指南的范围,但您最起码要知道的是:

  • 以一个 HTML 文件作为渲染器进程的入口点。
  • 使用层叠样式表 (Cascading Style Sheets, CSS) 对 UI 添加样式。
  • 通过 <script> 元素可添加可执行的 JavaScript 代码。

此外,这也意味着渲染器无权直接访问 require 或其他 Node.js API。 为了在渲染器中直接包含 NPM 模块,您必须使用与在 web 开发时相同的打包工具 (例如 webpackparcel)

:::警告

为了方便开发,可以用完整的 Node.js 环境生成渲染器进程。 在历史上,这是默认的,但由于安全原因,这一功能已被禁用。

:::

此刻,您或许会好奇:既然这些特性只能由主进程访问,那渲染器进程用户界面怎样才能与 Node.js 和 Electron 的原生桌面功能进行交互。 而事实上,确实没有直接导入 Electron 內容脚本的方法。

Preload 脚本

预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限。

预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程。

main.js

  1. const { BrowserWindow } = require('electron')
  2. //...
  3. const win = new BrowserWindow({
  4. webPreferences: {
  5. preload: 'path/to/preload.js',
  6. },
  7. })
  8. //...

因为预加载脚本与浏览器共享同一个全局 Window 接口,并且可以访问 Node.js API,所以它通过在全局 window 中暴露任意 API 来增强渲染器,以便你的网页内容使用。

虽然预加载脚本与其所附着的渲染器在共享着一个全局 window 对象,但您并不能从中直接附加任何变动到 window 之上,因为 contextIsolation 是默认的。

preload.js

  1. window.myAPI = {
  2. desktop: true,
  3. }

renderer.js

  1. console.log(window.myAPI)
  2. // => undefined

语境隔离(Context Isolation)意味着预加载脚本与渲染器的主要运行环境是隔离开来的,以避免泄漏任何具特权的 API 到您的网页内容代码中。

取而代之,我们將使用 contextBridge 模块来安全地实现交互:

preload.js

  1. const { contextBridge } = require('electron')
  2. contextBridge.exposeInMainWorld('myAPI', {
  3. desktop: true,
  4. })

renderer.js

  1. console.log(window.myAPI)
  2. // => { desktop: true }

此功能对两个主要目的來說非常有用:

  • 通过暴露 ipcRenderer 帮手模块于渲染器中,您可以使用 进程间通讯 ( inter-process communication, IPC ) 来从渲染器触发主进程任务 ( 反之亦然 ) 。
  • 如果您正在为远程 URL 上托管的现有 web 应用开发 Electron 封裝,则您可在渲染器的 window 全局变量上添加自定义的属性,好在 web 客户端用上仅适用于桌面应用的设计逻辑 。

The utility process

Each Electron app can spawn multiple child processes from the main process using the UtilityProcess API. The utility process runs in a Node.js environment, meaning it has the ability to require modules and use all of Node.js APIs. The utility process can be used to host for example: untrusted services, CPU intensive tasks or crash prone components which would have previously been hosted in the main process or process spawned with Node.js child_process.fork API. The primary difference between the utility process and process spawned by Node.js child_process module is that the utility process can establish a communication channel with a renderer process using MessagePorts. An Electron app can always prefer the UtilityProcess API over Node.js child_process.fork API when there is need to fork a child process from the main process.