跳转至

进程模型

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

为什么不是单进程?

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

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

多进程模型

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

Chrome 的多进程架构

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

主进程

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

窗口管理

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

每个 BrowserWindow 类实例都会创建一个应用程序窗口,该窗口在单独的渲染进程中加载一个 Web 页面。你可以使用窗口的 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)

[!NOTE] 对于 Web 嵌入(例如 BrowserView 模块)也会创建一个渲染进程。嵌入式 Web 内容也可以访问 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 的功能扩展到不仅仅是 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)。

:::warning

渲染进程可以以完整的 Node.js 环境派生,以便于开发。历史上,这曾经是默认行为,但由于安全原因,该功能已被禁用。

:::

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

预加载脚本

预加载脚本包含在渲染进程的 Web 内容开始加载之前执行的代码。这些脚本在渲染进程上下文中运行,但由于可以访问 Node.js API,因此被授予更多权限。

A preload script can be attached to the main process in the BrowserWindow constructor's webPreferences option.

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

由于预加载脚本与渲染进程共享全局 Window 接口,并且可以访问 Node.js API,因此它可以通过在 window 全局对象中暴露任意 API 来增强渲染进程,供你的 Web 内容使用。

尽管预加载脚本与其所附加的渲染进程共享 window 全局对象,但由于 contextIsolation 的默认设置,你不能直接将预加载脚本中的任何变量附加到 window 上。

```js title='preload.js' @ts-nocheck window.myAPI = { desktop: true }

```js title='renderer.js' @ts-nocheck
console.log(window.myAPI)
// => undefined

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

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

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

contextBridge.exposeInMainWorld('myAPI', {
  desktop: true
})

```js title='renderer.js' @ts-nocheck console.log(window.myAPI) // => { desktop: true }

此功能对于以下两个主要用途非常有用:

- 通过向渲染进程暴露 [`ipcRenderer`][ipcrenderer] 辅助函数,你可以使用进程间通信(IPC)从渲染进程触发主进程任务(反之亦然)。
- 如果你正在为托管在远程 URL 上的现有 Web 应用开发 Electron 封装器,你可以向渲染进程的 `window` 全局对象添加自定义属性,用于 Web 客户端侧的仅限桌面逻辑。

## 工具进程 {#the-utility-process}

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

## 进程特定模块别名(TypeScript) {#process-specific-module-aliases-typescript}

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

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

这些别名不会影响运行时,但可用于类型检查和自动补全。

```js title="Usage example"
const { shell } = require('electron/common')
const { app } = require('electron/main')

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 el/electron