跳到主要内容

进程模型

Electron 从 Chromium 继承了其多进程架构,这使得该框架在架构上与现代网络浏览器非常相似。本指南将扩展在教程中应用的概念。

为什么不是单一进程?

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

在早期,浏览器通常为所有这些功能使用单一进程。尽管这种模式意味着每个打开的选项卡开销较小,但这也意味着一个网站崩溃或挂起会影响整个浏览器。

多进程模型

为了解决这个问题,Chrome 团队决定每个选项卡都在自己的进程中渲染,从而限制了网页上存在bug或恶意代码对整个应用程序造成的损害。然后,一个单独的浏览器进程控制这些进程以及整个应用程序生命周期。下方来自 Chrome 漫画的图表可视化了这一模型

Chrome's multi-process architecture

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

主进程

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

窗口管理

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

BrowserWindow 类的每个实例都会创建一个应用程序窗口,该窗口在单独的渲染进程中加载网页。你可以使用窗口的 webContents 对象从主进程与此网页内容进行交互。

main.js
const { BrowserWindow } = require('electron')

const win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('https://github.com')

const contents = win.webContents
console.log(contents)
注意

还会为网页嵌入(例如 BrowserView 模块)创建渲染进程。对于嵌入式网页内容,webContents 对象也是可访问的。

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

BrowserWindow 实例被销毁时,其对应的渲染进程也会终止。

应用程序生命周期

主进程还通过 Electron 的 app 模块控制应用程序的生命周期。该模块提供了一大套事件和方法,你可以使用它们来添加自定义应用程序行为(例如,通过编程方式退出应用程序、修改应用程序停靠栏或显示“关于”面板)。

作为一个实际示例,教程启动代码中显示的应用程序使用 app API 来创建更原生的应用程序窗口体验。

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

原生 API

为了将 Electron 的功能扩展到作为网页内容的 Chromium 包装器之外,主进程还添加了自定义 API 来与用户的操作系统进行交互。Electron 暴露了各种模块来控制原生桌面功能,例如菜单、对话框和托盘图标。

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

渲染进程

每个 Electron 应用程序为每个打开的 BrowserWindow(以及每个网页嵌入)生成一个单独的渲染进程。顾名思义,渲染器负责渲染网页内容。就所有意图和目的而言,在渲染进程中运行的代码应根据网络标准行事(至少在 Chromium 中是这样)。

因此,单个浏览器窗口中的所有用户界面和应用程序功能都应使用你在网络上使用的相同工具和范例来编写。

虽然解释每个 Web 规范超出了本指南的范围,但最低限度需要理解的是:

  • HTML 文件是渲染进程的入口点。
  • UI 样式通过层叠样式表 (CSS) 添加。
  • 可执行的 JavaScript 代码可以通过 <script> 元素添加。

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

警告

渲染进程可以带着完整的 Node.js 环境生成,以方便开发。从历史上看,这曾是默认设置,但出于安全原因,此功能已被禁用。

此时,你可能会想,如果这些功能只能从主进程访问,那么渲染进程用户界面如何与 Node.js 和 Electron 的原生桌面功能进行交互。实际上,没有直接的方法来导入 Electron 的内容脚本。

预加载脚本

预加载脚本包含在网页内容开始加载之前在渲染进程中执行的代码。这些脚本在渲染器上下文中运行,但由于能够访问 Node.js API 而被授予更多特权。

预加载脚本可以附加到主进程的 BrowserWindow 构造函数的 webPreferences 选项中。

main.js
const { BrowserWindow } = require('electron')
// ...
const win = new BrowserWindow({
webPreferences: {
preload: 'path/to/preload.js'
}
})
// ...

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

尽管预加载脚本与它们所附加的渲染器共享一个 window 全局对象,但由于默认启用contextIsolation,你无法直接将预加载脚本中的任何变量附加到 window

preload.js
window.myAPI = {
desktop: true
}
renderer.js
console.log(window.myAPI)
// => undefined

上下文隔离意味着预加载脚本与渲染器的主世界隔离,以避免任何特权 API 泄露到你的网页内容代码中。

相反,请使用 contextBridge 模块来安全地实现这一点

preload.js
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('myAPI', {
desktop: true
})
renderer.js
console.log(window.myAPI)
// => { desktop: true }

此功能主要有两个非常实用的用途:

  • 通过向渲染器暴露 ipcRenderer 助手,你可以使用进程间通信 (IPC) 从渲染器触发主进程任务(反之亦然)。
  • 如果你正在为托管在远程 URL 上的现有 Web 应用程序开发 Electron 包装器,你可以在渲染器的 window 全局对象上添加自定义属性,这些属性可用于 Web 客户端的桌面专用逻辑。

工具进程

每个 Electron 应用程序都可以使用 UtilityProcess API 从主进程生成多个子进程。工具进程在 Node.js 环境中运行,这意味着它能够 require 模块并使用所有 Node.js API。工具进程可用于托管例如:不受信任的服务、CPU 密集型任务或容易崩溃的组件,这些组件以前可能托管在主进程或使用 Node.js child_process.fork API 生成的进程中。工具进程与 Node.js child_process 模块生成的进程之间的主要区别在于,工具进程可以使用 MessagePorts 与渲染进程建立通信通道。当需要从主进程派生子进程时,Electron 应用程序可以始终优先选择 UtilityProcess API 而不是 Node.js child_process.fork API。

进程特定模块别名 (TypeScript)

Electron 的 npm 包还导出了包含 Electron TypeScript 类型定义子集的分支路径。

  • electron/main 包含所有主进程模块的类型。
  • electron/renderer 包含所有渲染进程模块的类型。
  • electron/common 包含可以在主进程和渲染进程中运行的模块的类型。

这些别名对运行时没有影响,但可用于类型检查和自动完成。

使用示例
const { app } = require('electron/main')
const { shell } = require('electron/common')