进程模型
Electron 从 Chromium 继承了其多进程架构,这使得该框架在架构上与现代 Web 浏览器非常相似。本指南将扩展 教程 中应用的概念。
为什么不是单进程?
Web 浏览器是非常复杂的应用程序。除了显示 Web 内容的主要功能外,它们还有许多次要职责,例如管理多个窗口(或标签页)和加载第三方扩展程序。
在早期,浏览器通常使用单个进程来处理所有这些功能。虽然这种模式意味着每个打开的标签页的开销较小,但也意味着一个网站崩溃或挂起会影响整个浏览器。
多进程模型
为了解决这个问题,Chrome 团队决定每个标签页都在其自己的进程中渲染,从而限制了 Web 页面上错误或恶意代码可能对整个应用程序造成的损害。然后,单个浏览器进程控制这些进程以及整个应用程序生命周期。下面来自 Chrome Comic 的图表可视化了这个模型
Electron 应用程序的结构非常相似。作为应用程序开发者,您控制两种类型的进程:主进程和 渲染器进程。这些进程类似于 Chrome 自己的浏览器和上面概述的渲染器进程。
主进程
每个 Electron 应用程序都有一个单主进程,它充当应用程序的入口点。主进程在 Node.js 环境中运行,这意味着它能够 require
模块并使用所有 Node.js API。
窗口管理
主进程的主要目的是使用 BrowserWindow
模块创建和管理应用程序窗口。
BrowserWindow
类的每个实例都会创建一个应用程序窗口,该窗口在单独的渲染器进程中加载 Web 页面。您可以使用窗口的 webContents
对象从主进程与此 Web 内容进行交互。
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
模块)创建。webContents
对象也可用于嵌入式 Web 内容。
由于 BrowserWindow
模块是一个 EventEmitter
,您还可以为各种用户事件(例如,最小化或最大化窗口)添加处理程序。
当 BrowserWindow
实例被销毁时,其对应的渲染器进程也会被终止。
应用程序生命周期
主进程还通过 Electron 的 app
模块控制应用程序的生命周期。此模块提供了一大组事件和方法,您可以使用它们来添加自定义应用程序行为(例如,以编程方式退出应用程序、修改应用程序坞或显示“关于”面板)。
作为一个实际示例,教程入门代码 中显示的应用程序使用 app
API 来创建更原生的应用程序窗口体验。
// 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 文件是渲染器进程的入口点。
- UI 样式通过层叠样式表 (CSS) 添加。
- 可执行 JavaScript 代码可以通过
<script>
元素添加。
此外,这也意味着渲染器无法直接访问 require
或其他 Node.js API。为了在渲染器中直接包含 NPM 模块,您必须使用与在 Web 上使用的相同的捆绑器工具链(例如,webpack
或 parcel
)。
渲染器进程可以生成一个完整的 Node.js 环境,以方便开发。从历史上看,这曾经是默认设置,但出于安全原因,此功能已被禁用。
此时,您可能想知道如果这些功能只能从主进程访问,那么渲染器进程用户界面如何与 Node.js 和 Electron 的原生桌面功能进行交互。实际上,没有直接导入 Electron 内容脚本的方法。
预加载脚本
预加载脚本包含在 Web 内容开始加载之前在渲染器进程中执行的代码。这些脚本在渲染器上下文中运行,但由于可以访问 Node.js API 而被授予更多特权。
预加载脚本可以附加到主进程,方法是在 BrowserWindow
构造函数的 webPreferences
选项中进行设置。
const { BrowserWindow } = require('electron')
// ...
const win = new BrowserWindow({
webPreferences: {
preload: 'path/to/preload.js'
}
})
// ...
由于预加载脚本与渲染器共享全局 Window
接口,并且可以访问 Node.js API,因此它通过在您的 Web 内容可以使用的 window
全局中公开任意 API 来增强您的渲染器。
虽然预加载脚本与它们附加到的渲染器共享一个 window
全局对象,但由于 contextIsolation
默认设置,您无法直接将任何变量从预加载脚本附加到 window
。
window.myAPI = {
desktop: true
}
console.log(window.myAPI)
// => undefined
上下文隔离意味着预加载脚本与渲染器的主世界隔离,以避免将任何特权 API 泄漏到您的 Web 内容代码中。
相反,使用 contextBridge
模块可以安全地完成此操作
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
desktop: true
})
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 模块生成的进程之间的主要区别在于,实用程序进程可以使用 MessagePort
与渲染器进程建立通信通道。当需要从主进程 fork 一个子进程时,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')