跳到主要内容

进程模型

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

为何不使用单个进程?

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

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

多进程模型

为了解决这个问题,Chrome 团队决定每个标签页都在自己的进程中渲染,从而限制了网页上的错误或恶意代码对整个应用程序造成的损害。然后,单个浏览器进程控制这些进程以及整个应用程序生命周期。Chrome Comic 中的以下图示可视化了这个模型

Chrome's multi-process architecture

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

主进程

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

窗口管理

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

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

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)

注意:也会为Web 嵌入创建渲染器进程,例如 BrowserView 模块。对于嵌入式 Web 内容,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 的功能,使其不仅仅是 Web 内容的 Chromium 包装器,主进程还添加了自定义 API 来与用户的操作系统交互。Electron 暴露了各种控制原生桌面功能的模块,例如菜单、对话框和托盘图标。

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

渲染器进程

每个 Electron 应用程序为每个打开的 BrowserWindow(以及每个 Web 嵌入)创建一个单独的渲染器进程。顾名思义,渲染器负责渲染 Web 内容。就所有目的而言,在渲染器进程中运行的代码应符合 Web 标准(至少 Chromium 是这样)。

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

尽管解释每一个 Web 规范超出了本指南的范围,但至少需要理解的是

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

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

警告

渲染器进程可以运行在完整的 Node.js 环境中,以方便开发。历史上,这曾经是默认设置,但出于安全原因此功能已被禁用。

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

预加载脚本

预加载脚本包含在渲染器进程中 Web 内容开始加载之前执行的代码。这些脚本在渲染器上下文中运行,但由于可以访问 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 来增强你的渲染器,你的 Web 内容随后可以消费这些 API。

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

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

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

相反,使用 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')