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