跳转到主要内容

进程模型

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

为什么不采用单进程模式?

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

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

多进程模型

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

Chrome's multi-process architecture

Electron 应用程序的结构也非常相似。作为应用程序开发人员,您可以控制两种类型的进程:主进程 (main)渲染进程 (renderer)。它们分别对应于上面概述的 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 模块控制您的应用程序的生命周期。该模块提供了一系列事件和方法,您可以使用它们来添加自定义的应用程序行为(例如,以编程方式退出应用程序、修改应用程序的 Dock 或显示关于面板)。

作为一个实际示例,教程入门代码中展示的应用程序使用 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(以及每个网页嵌入)都会启动一个单独的渲染进程。顾名思义,渲染进程负责渲染网页内容。在所有意图和目的上,在渲染进程中运行的代码都应该符合 Web 标准(至少在 Chromium 的能力范围内)。

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

虽然解释每个 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,因此它可以将任意 API 暴露给 window 全局,从而增强您的渲染进程,使您的网页内容可以进行消费。

尽管预加载脚本与它们附加到的渲染进程共享一个 window 全局,但由于默认的 contextIsolation,您无法直接将预加载脚本中的任何变量附加到 window

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

上下文隔离 (Context Isolation) 意味着预加载脚本与渲染器的主要世界是隔离的,以避免将任何特权 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 { shell } = require('electron/common')
const { app } = require('electron/main')