主进程与渲染进程
本教程共 45 篇 · 第 4 篇 · 更新于 2026-08-03
4. 主进程与渲染进程
本节目标
- 理解 Electron 为何采用多进程架构而非单进程。
- 弄清主进程、渲染进程、Preload 三者的职责与关系。
- 明白渲染进程默认没有 Node 能力的原因。
- 理解为何跨进程交互必须依靠 IPC。
- 认识 utility process 的用途与适用场景。
上一章你跑通了窗口,但可能还模糊:为什么 main.js 能 require,页面里却不行?这一章把 Electron 的进程模型讲透。理解了它,后面所有 API 的归属就清楚了。
4-1 为什么不是单进程
早期浏览器把所有标签页塞进一个进程。一个页面崩溃,整个浏览器跟着死。Chromium 把每个标签页拆成独立进程,单个页面出问题不会拖垮全局。
Electron 沿用了这套多进程思路。它把”管应用”和”显界面”两件事分到不同进程,既稳又能隔离风险。这也是它和早期 NW.js 最大的架构区别。
4-2 主进程:应用的入口与大脑
每个 Electron 应用有且仅有一个主进程,由 package.json 的 main 字段指定。它跑在 Node.js 环境里,能 require 任意模块、用全部 Node API。
主进程的核心职责有三类:
- 用
BrowserWindow创建和管理窗口。 - 用
app模块掌控应用生命周期。 - 调用菜单、对话框、托盘这类原生桌面能力。
// 主进程 main.js
const { app, BrowserWindow } = require('electron/main')
app.whenReady().then(() => {
const win = new BrowserWindow({ width: 800, height: 600 })
win.loadFile('index.html')
// webContents 让主进程能操作窗口里的网页内容
win.webContents.on('did-finish-load', () => {
console.log('页面加载完成')
})
})
每个 BrowserWindow 实例都会附带一个独立的渲染进程。窗口被销毁时,对应的渲染进程也随之结束。
4-3 渲染进程:负责画界面
每个打开的 BrowserWindow 都会产生一个渲染进程。它做的事就是渲染网页内容,规则和你在浏览器里写前端完全一致。
因此界面、样式、交互逻辑,用的都是 HTML、CSS 和浏览器标准 JavaScript。你甚至可以用 webpack、React 这些前端工具链来组织代码。
<!-- 渲染进程入口就是普通网页 -->
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8" /><title>渲染进程</title></head>
<body>
<h1>这是渲染进程画的界面</h1>
</body>
</html>
关键点来了:渲染进程默认拿不到 require,也用不了 Node API。历史上 Electron 曾默认开放,后来因安全问题关掉了。这正引出了 preload 的价值。
4-4 Preload:处在中间的桥梁
preload 脚本在页面内容加载前执行,运行在渲染进程上下文,却额外获准使用 Node API。它共享页面的 window 全局对象。
但直接往 window 挂变量是不行的,因为 contextIsolation 默认开启,preload 和页面处在隔离的上下文里。直接挂会拿不到:
// preload.js —— 不要这样写
window.myAPI = { desktop: true }
// renderer.js —— 拿到的是 undefined
console.log(window.myAPI) // => undefined
正确做法是用 contextBridge 安全暴露。隔离机制保证页面里的恶意代码碰不到 preload 的特权接口。
// preload.js —— 正确写法
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
desktop: true
})
// renderer.js —— 现在能拿到了
console.log(window.myAPI) // => { desktop: true }
4-5 三者关系一张图
把三者摆在一起看:主进程是大脑,渲染进程是脸面,preload 是神经。主进程管窗口与系统,渲染进程画界面,preload 把两者受控地连起来。
主进程 (Node.js)
│ 创建 BrowserWindow
▼
渲染进程 (网页) ◀── preload 桥梁 ──▶ 主进程能力
一个窗口一个渲染进程,多个窗口就有多个渲染进程。它们彼此独立,互不影响,崩溃也只崩自己那一个。
4-6 为什么必须靠 IPC 通信
既然主进程和渲染进程是分开的,页面想做”读文件""弹对话框”这类事,就得请主进程代劳。两者之间的通信叫 IPC(Inter-Process Communication)。
渲染进程不能直接调主进程函数,主进程也不能直接改页面 DOM。所有跨进程的数据交换,都走 ipcRenderer 和 ipcMain 这对通道。
// preload.js —— 暴露一个请求能力
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
readConfig: () => ipcRenderer.invoke('read-config')
})
// main.js —— 主进程响应
const { ipcMain } = require('electron/main')
const fs = require('node:fs')
const path = require('node:path')
ipcMain.handle('read-config', () => {
// 用 __dirname 拼绝对路径,别依赖启动时的工作目录
return fs.readFileSync(path.join(__dirname, 'config.json'), 'utf-8')
})
这套”请求—响应”模式是 Electron 的通信主干。把特权操作收口在主进程,页面只拿到必要结果,安全边界就守住了。
4-7 还有个 utility process
除了主进程和渲染进程,Electron 还允许从主进程派生 utility process。它同样跑在 Node.js 环境,适合承载耗时计算、易崩溃组件,避免拖慢主进程。
它和 Node 的 child_process.fork 类似,但额外能跟渲染进程用 MessagePort 建立通道。普通应用用不到,了解有这个选项即可。
4-8 进程间边界意味着什么
隔离带来的直接后果是:主进程里的变量,渲染进程看不到;页面的 DOM,主进程改不了。两者各自独立,只能通过 IPC 传递可序列化的数据。
这意味着你不能把函数、DOM 节点这类对象直接跨进程传。IPC 通道只认能被结构化克隆的数据,比如字符串、数字、数组、普通对象。想传复杂对象,先把它压平成 JSON 友好的结构。
传不过去时报错信息往往含糊,排查思路很固定:把要传的值先 JSON.stringify 一遍试试,能序列化就基本能过桥,报错就说明里面混进了函数、类实例或循环引用。
4-9 常见误区
关于进程模型,最常见的误解是认为渲染进程能直接用 Node。在较早的 Electron 版本里这曾是默认行为,出于安全考虑早已关闭。现在的默认是渲染进程就是网页,特权操作全收口在主进程。
另一个误区是滥用 remote 思路。Electron 14 起 remote 模块已被移除,v43 里完全不存在。想从页面调主进程能力,正道是用 contextBridge 加 ipcRenderer,不要去找 remote 的替代写法。
4-10 进程模型与稳定性
多进程带来的最大好处是隔离故障。某个渲染进程因为脚本错误卡死,不会拖垮主进程和其他窗口,用户关掉那个窗口即可恢复。
正因如此,把容易崩溃或计算密集的活儿放进独立进程是常见实践。Electron 提供的 utility process 就专为这类场景设计,它和主进程一样跑 Node,却不会在主进程里抢资源。
理解这套模型后,你写代码时会自然地把”界面”和”能力”分开:界面交给渲染进程,敏感的系统操作收口在主进程,中间用 preload 这座受控的桥连起来。
4-11 本章小结
Electron 是多进程架构:主进程管生命周期与原生能力,渲染进程画界面,preload 做受控桥梁。渲染进程默认没有 Node 能力,跨进程交互一律走 IPC。
记住隔离是好事,不是麻烦。下一章我们看 package.json 与脚本如何配置。