02 · 进程模型:主进程、渲染进程与 Preload

目标:理解 Electron 的多进程架构(继承自 Chromium),掌握 preload + contextBridge 的安全桥接模式。 官方对应:流程模型、使用预加载脚本、上下文隔离

架构总览

┌─────────────────────────────────────────────┐
│  主进程 (main process)  ← 唯一,Node 环境      │
│  - app 生命周期 / BrowserWindow 窗口管理       │
│  - 原生 API:菜单、托盘、对话框、通知...        │
│  - 可 require 任意 Node 模块                  │
├─────────────────────────────────────────────┤
│  渲染进程 (renderer)  ← 每窗口一个,Chromium   │
│  - HTML/CSS/JS,行为遵循 Web 标准             │
│  - 默认无 Node 权限(历史默认有,现已禁用)      │
│  - 需要打包工具(webpack/vite)引入 npm 包     │
├─────────────────────────────────────────────┤
│  实用进程 (utility)  ← 可选,UtilityProcess   │
│  - 托管不受信任服务 / CPU 密集任务 / 易崩溃组件  │
│  - 可用 MessagePort 与渲染进程直连             │
└─────────────────────────────────────────────┘

为什么多进程:单进程浏览器里一个标签页崩溃会拖垮整个应用;Chrome 把每个标签页隔离到独立渲染进程,Electron 沿用了这一架构。BrowserWindow 实例销毁时,对应渲染进程也被终止。

主进程三大职责:

  1. 窗口管理:每个 BrowserWindow 一个渲染进程,通过 win.webContents 与页面内容交互(webContents 也是 EventEmitter,可监听最小化/最大化等事件)
  2. 应用生命周期:app 模块提供全套事件/方法(退出、dock、关于面板)
  3. 原生 API:菜单、对话框、托盘图标等桌面特性

实例:暴露版本号到页面

preload.js — 安全暴露 API

const { contextBridge } = require('electron')
 
contextBridge.exposeInMainWorld('versions', {
  node: () => process.versions.node,
  chrome: () => process.versions.chrome,
  electron: () => process.versions.electron
  // 除函数之外,也可以暴露变量
})

main.js — 挂载 preload

const { app, BrowserWindow } = require('electron')
const path = require('node:path')
 
const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })
  win.loadFile('index.html')
}
 
app.whenReady().then(() => createWindow())

renderer.js — 页面里使用

const information = document.getElementById('info')
information.innerText = `本应用使用 Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), Electron (v${versions.electron()})`

运行后窗口会显示三者的版本号。__dirname 指向当前脚本目录,path.join 拼跨平台路径。

上下文隔离(contextIsolation)

默认开启(Electron 12+)。preload 与页面运行在隔离的 JS 上下文中:

// preload.js 里直接赋值 —— 无效!
window.myAPI = { desktop: true }
 
// renderer.js
console.log(window.myAPI) // => undefined

唯一正确的暴露方式是 contextBridge.exposeInMainWorld()。这样做的意义:防止恶意页面代码篡改原型链(Array.prototype.push、JSON.parse 等)来窃取你暴露的特权 API。

Preload 脚本的沙箱限制

Electron 20+ preload 默认沙箱化,没有完整 Node 环境,只有一个 polyfill 的 require,可用 API 受限:

类别可用
Electron 模块渲染进程相关模块
Node 模块events、timers、url
全局 polyfillBuffer、process、clearImmediate、setImmediate

所以 preload 里不要指望 fs/path/child_process——需要系统能力的逻辑放主进程,通过 IPC 调用(见 03-ipc)。

典型三层分工(心法)

层环境职责关键词
main.jsNode + Electron系统能力、窗口、生命周期特权
preload.js受限 Node + DOM桥:contextBridge 暴露白名单 API关卡
renderer.js纯浏览器UI 与交互沙箱

TypeScript 类型化导入别名

const { app } = require('electron/main')       // 主进程模块
const { ipcRenderer } = require('electron/renderer')
const { shell } = require('electron/common')   // 双端通用

仅影响类型检查/自动补全,运行时无影响。写 TS 时建议用,能防止在主进程里误用渲染器模块。

练习

  1. 在 preload 里再暴露一个 platform: () => process.platform,页面显示当前操作系统
  2. 试试直接 window.foo = 1(preload 里)然后页面读取,验证上下文隔离
  3. 阅读 进程沙盒化 与 消息端口(进阶)

小结

  • 主进程 = 特权层;渲染进程 = 浏览器层;两者靠 IPC 通信
  • contextBridge 是唯一的特权暴露通道,且默认上下文隔离
  • preload 本身也被沙箱限制,重活放主进程
  • 下一章:03-ipc — 四种 IPC 模式完整实例