Electron 流程模型与多进程架构详解

Viewed 0

流程模型

Electron 继承了来自 Chromium 的多进程架构,这使得此框架在架构上非常相似于一个现代的网页浏览器。本指南将扩展教程中使用的概念,深入探讨其核心机制。

为什么不是一个单一的进程?

网页浏览器是个极其复杂的应用程序。除了显示网页内容的主要能力之外,他们还有许多次要的职责,例如管理众多窗口或标签页以及加载第三方扩展。在早期,浏览器通常使用单个进程来处理所有这些功能。虽然这种模式意味着您打开每个标签页的开销较少,但也同时意味着一个网站的崩溃或无响应会影响到整个浏览器。

多进程模型

为了解决这个问题,Chrome 团队决定让每个标签页在自己的进程中渲染,从而限制了一个网页上的有误或恶意代码可能导致的对整个应用程序造成的伤害。然后用单个浏览器进程控制这些标签页进程,以及整个应用程序的生命周期。Electron 应用程序的结构非常相似。作为应用开发者,你将控制两种类型的进程:主进程和渲染器进程。这类似于 Chrome 的浏览器和渲染器进程。

主进程

每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。

窗口管理

主进程的首要目的是使用 BrowserWindow 模块创建和管理应用程序窗口。BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。您可以在主进程中用 window 的 webContents 对象与网页内容进行交互。

例如,在主进程中创建窗口并加载 URL:

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 的功能不仅仅限于对网页内容的封装,主进程也添加了自定义的 API 来与用户的作业系统进行交互。Electron 有着多种控制原生桌面功能的模块,例如菜单、对话框以及托盘图标。关于 Electron 主进程模块的完整列表,请参阅 API 文档。

渲染器进程

每个 Electron 应用都会为每个打开的 BrowserWindow 或每个网页嵌入生成一个单独的渲染器进程。渲染器负责渲染网页内容。本质上,在渲染进程内运行的代码的行为应当遵循 Web 标准。

因此,一个浏览器窗口中的所有的用户界面和应用功能,都应与您在网页开发上使用相同的工具和规范来进行编写。虽然解释每一个网页规范超出了本指南的范围,但您最起码要知道的是:以一个 HTML 文件作为渲染器进程的入口点,使用层叠样式表对 UI 添加样式,并通过 <script> 元素可添加可执行的 JavaScript 代码。

此外,这也意味着渲染器无权直接访问 require 或其他 Node.js API。为了在渲染器中直接包含 NPM 模块,您必须使用与在 web 开发时相同的打包工具,例如 webpackparcel

为了方便开发,可以用完整的 Node.js 环境生成渲染器进程。在历史上,这是默认的,但由于安全原因,这一功能已被禁用。

Preload 脚本

预加载脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码。这些脚本虽运行于渲染器的环境中,却因能访问 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 来增强渲染器,以便你的网页内容使用。然而,由于 contextIsolation 默认启用,您不能在预加载脚本中直接将变量附加到 window 上。

作为替代,请使用 contextBridge 模块来安全地实现这一目的。例如,在预加载脚本中:

const { contextBridge } = require('electron')

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

然后在渲染器中:

console.log(window.myAPI)
// => { desktop: true }

此功能对两个主要目的來說非常有用:通过向渲染器暴露 ipcRenderer 帮助程序,您可以使用进程间通信从渲染器触发主进程的任务;如果您正在为远程 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 与渲染进程建立通信通道。当 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')
0 Answers