前端知库

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 暴露给页面;开启沙箱后它只能 require electron 模块中的一部分 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 进程

常见追问