Electron 的主进程、渲染进程、preload 和 utility 进程分别负责什么?
说明 Electron 多进程模型中各进程的职责、可用 API 和生命周期,以及什么时候把任务放到 utility 进程或 Web Worker。
参考答案
简答
- 主进程每个应用只有一个,运行在 Node.js 环境,负责应用生命周期(
app)、创建和管理窗口(BrowserWindow)以及菜单、对话框、托盘等原生能力。 - 每个
BrowserWindow和网页嵌入都有自己的渲染进程,按 Web 标准运行 HTML、CSS 和 JS,默认不能直接使用require和 Node.js API。 - preload 脚本在页面内容加载前运行在渲染进程中,借助
contextBridge.exposeInMainWorld把有限的 API 暴露给页面;开启沙箱后它只能requireelectron模块中的一部分 API。 - utility 进程由主进程通过
utilityProcess.fork创建,拥有完整 Node.js 能力,适合 CPU 密集任务、不可信服务和容易崩溃的组件,接口和child_process.fork相似,还能通过MessagePort和渲染进程直接通信。 - 进程之间通过 IPC 或
MessagePort通信,主进程要保持空闲,不在主进程里做长时间同步计算。
Electron 为什么是多进程
Electron 继承了 Chromium 的多进程架构。Chromium 把每个页面放进单独的渲染进程,一个页面卡死或崩溃不会拖垮整个浏览器。Electron 在此基础上增加了一个运行 Node.js 的主进程来管理整个应用。
主进程
- 应用入口(
package.json的main)在主进程执行,可以使用全部 Node.js API。 - 用
BrowserWindow创建窗口,每个窗口在独立的渲染进程中加载页面,主进程通过窗口的webContents控制它。窗口销毁时对应的渲染进程也会结束。 - 通过
app模块控制退出、Dock 图标等生命周期行为,通过Menu、dialog、Tray等模块使用原生界面。
主进程负责所有窗口的事件分发和进程间通信,它一旦被阻塞,整个应用的窗口都会失去响应。
渲染进程
渲染进程的代码和普通网页一样:入口是 HTML,用 <script> 加载 JS,用打包工具引入 npm 包。出于安全原因,渲染进程默认不再拥有 Node.js 能力,nodeIntegration 默认关闭,沙箱自 Electron 20 起默认开启。
preload 脚本
JavaScript
// main.js
new BrowserWindow({
webPreferences: { preload: path.join(__dirname, 'preload.js') },
});
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', {
openFile: () => ipcRenderer.invoke('dialog:openFile'),
});
contextIsolation默认开启,preload 和页面处在不同的 JS 上下文,直接给window赋值页面看不到,需要通过contextBridge暴露。- 沙箱模式下 preload 只能
require('electron'),可用的有contextBridge、crashReporter、ipcRenderer、nativeImage、webFrame、webUtils,不能用 Node.js 内置模块。文件读写等操作通过 IPC 交给主进程。
utility 进程
JavaScript
// main.js,需要在 app 的 ready 事件之后调用
const { utilityProcess } = require('electron');
const child = utilityProcess.fork(path.join(__dirname, 'worker.js'), [], {
serviceName: 'Image Worker',
});
child.postMessage({ type: 'resize', file });
child.on('message', (result) => { /* 处理结果 */ });
child.on('exit', (code) => { /* 崩溃或退出后决定是否重启 */ });
// worker.js
process.parentPort.on('message', (e) => {
process.parentPort.postMessage(doHeavyWork(e.data));
});
- utility 进程基于 Chromium 的 Services API 启动,接口和
child_process.fork相似。和child_process.fork相比,主要区别是它可以通过MessagePort和渲染进程直接建立通信通道。 serviceName会出现在app.getAppMetrics()和child-process-gone事件中,方便监控和定位。- 只有 stdout 和 stderr 可以配置,stdin 必须是
ignore。
怎么选择执行位置
| 任务 | 推荐位置 |
|---|---|
| 界面渲染、交互逻辑 | 渲染进程 |
| 页面内的长计算(解析、压缩) | 渲染进程中的 Web Worker |
| 文件系统、原生对话框、窗口管理 | 主进程,经 IPC 调用 |
| 需要 Node.js 的重计算、第三方原生模块、可能崩溃的服务 | utility 进程 |
常见追问
- 多个窗口怎么共享状态?由主进程或 utility 进程持有状态,再通过 IPC 广播,或者用
MessagePort让窗口之间直接通信,通信方式见《Electron 的 IPC 和 contextBridge 怎样设计?》。 - 为什么不在 preload 里直接做文件操作?沙箱开启后 preload 拿不到
fs;即使关闭沙箱,把文件能力放进渲染进程也会扩大 XSS 的影响,安全细节见《Electron 应用的主进程、渲染进程和 IPC 有哪些安全边界?》。