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 实例销毁时,对应渲染进程也被终止。
主进程三大职责:
- 窗口管理:每个
BrowserWindow一个渲染进程,通过win.webContents与页面内容交互(webContents也是EventEmitter,可监听最小化/最大化等事件) - 应用生命周期:
app模块提供全套事件/方法(退出、dock、关于面板) - 原生 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 |
| 全局 polyfill | Buffer、process、clearImmediate、setImmediate |
所以 preload 里不要指望 fs/path/child_process——需要系统能力的逻辑放主进程,通过 IPC 调用(见 03-ipc)。
典型三层分工(心法)
| 层 | 环境 | 职责 | 关键词 |
|---|---|---|---|
| main.js | Node + 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 时建议用,能防止在主进程里误用渲染器模块。
练习
- 在 preload 里再暴露一个
platform: () => process.platform,页面显示当前操作系统 - 试试直接
window.foo = 1(preload 里)然后页面读取,验证上下文隔离 - 阅读 进程沙盒化 与 消息端口(进阶)
小结
- 主进程 = 特权层;渲染进程 = 浏览器层;两者靠 IPC 通信
contextBridge是唯一的特权暴露通道,且默认上下文隔离- preload 本身也被沙箱限制,重活放主进程
- 下一章:03-ipc — 四种 IPC 模式完整实例