Skip to content
基本概念:主进程与渲染进程
概述
Electron 的进程模型直接继承自 Chromium。现代浏览器把网络请求、GPU 渲染、页面解析等任务分发到独立的沙箱进程中,一个标签页崩溃不会拖垮整个浏览器窗口。Electron 把同样的架构搬进了桌面应用:主进程相当于浏览器自身,渲染进程则对应打开的标签页。
分清这两个进程的职责边界是开发 Electron 应用的基础。主进程负责系统级能力,渲染进程负责界面呈现,两者之间通过 IPC 通信。如果混淆了边界,就容易写出在渲染进程里直接调用 Node.js 文件系统 API 的代码——这在实际运行中是行不通的,因为渲染进程默认不具备 Node.js 环境。
基本概念
双进程模型
┌─────────────────────────────────────────────┐
│ 主进程 │
│ Node.js 环境 │
│ ├─ app 模块(生命周期) │
│ ├─ BrowserWindow 管理 │
│ └─ 系统 API(菜单/托盘/对话框/文件系统) │
│ │ │
│ │ IPC(进程间通信) │
│ │ │
│ ┌───┴────────┐ ┌────────────┐ │
│ │ 渲染进程 1 │ │ 渲染进程 2 │ ... │
│ │ Chromium │ │ Chromium │ │
│ │ HTML/CSS/JS│ │ HTML/CSS/JS│ │
│ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────┘一个主进程可以管理多个渲染进程,每个渲染进程对应一个 BrowserWindow 实例,各自加载独立的页面。进程之间不共享内存空间,这是 Chromium 沙箱机制的直接结果。
主进程
主进程是整个应用的入口。package.json 中 main 字段指定的脚本就运行在这个进程里,Electron 启动后立即执行它。整个应用有且仅有一个主进程,一旦它退出,应用就结束了。
主进程运行在 Node.js 环境中,require、fs、path、process 等模块全部可用。这是它与渲染进程最根本的环境差异——后者默认不具备这些能力。
app 模块控制着应用的生命周期。它暴露了一系列事件,覆盖从启动到退出的各个阶段:
ready:Electron 初始化完成,可以开始创建窗口。在此之前创建BrowserWindow实例会出错。window-all-closed:所有窗口关闭时触发。Windows 和 Linux 上通常在此调用app.quit()退出,macOS 习惯保留 Dock 图标不退出。before-quit:在app.quit()之后、窗口开始关闭之前触发。
app.whenReady() 返回一个 Promise,是目前推荐的入口写法。旧的 app.on('ready', callback) 方式仍然有效,但在复杂初始化流程里不如 Promise 方便组合。
js
const { app } = require('electron');
app.whenReady().then(() => {
// 此时可以安全创建 BrowserWindow
console.log('应用就绪');
});渲染进程
渲染进程由 Chromium 创建,每个 BrowserWindow 实例对应一个独立的渲染进程。它负责加载 HTML、CSS 和 JavaScript,并按照 Web 标准将页面渲染出来。
渲染进程里没有 require。fs、path、process 等 Node.js 模块默认不可用。页面代码按照浏览器规则执行——window、document、fetch、localStorage 正常存在,但 require('fs') 会直接抛出错误。
一个渲染进程的生命周期绑定在它的宿主窗口上。调用 win.close() 或用户点击关闭按钮后,窗口被销毁,对应的渲染进程就会被终止。进程内的所有状态——DOM 树、JavaScript 对象、定时器——全部丢失。跨窗口的状态持久化要么写入磁盘,要么交给主进程托管。
BrowserWindow 与 webContents
BrowserWindow 在主进程中创建,但它本身不是一个进程。它是一个配置对象,指定窗口的尺寸、是否可调整大小、要加载哪个页面等参数。Electron 根据这些参数在底层创建一个 Chromium 窗口,并在独立的渲染进程中加载指定的 HTML 文件或 URL。
每个 BrowserWindow 实例都带有一个 webContents 属性,它是与页面内容交互的接口。webContents 负责导航、页面加载事件和脚本执行等。主进程通过 win.webContents.loadURL() 或 win.loadFile() 加载页面,通过事件监听页面加载完成的状态。
js
const win = new BrowserWindow({
width: 1024,
height: 768,
});
// 加载本地文件
win.loadFile('renderer/index.html');
// 或者加载远程 URL
// win.loadURL('https://example.com');
// 监听页面加载完成
win.webContents.on('did-finish-load', () => {
console.log('页面加载完成');
});webContents 还能在主进程中向页面注入 JavaScript 或监听页面内的控制台输出,但它不能直接操作 DOM。DOM 在渲染进程里,主进程拿到的只是页面内容的代理。
一主多渲染
主进程可以多次调用窗口创建函数,每次调用都会新建一个 BrowserWindow 实例,进而启动一个新的渲染进程。
js
app.whenReady().then(() => {
const mainWin = new BrowserWindow({ width: 800, height: 600 });
mainWin.loadFile('main.html');
const settingsWin = new BrowserWindow({ width: 600, height: 400 });
settingsWin.loadFile('settings.html');
console.log(BrowserWindow.getAllWindows().length); // 2
});此时进程树的结构是:一个主进程,两个渲染进程。两个窗口的全局变量互不影响。如果其中一个窗口因为页面死循环而崩溃,另一个窗口不受影响,主进程也不会挂掉。
需要跨窗口共享数据时,常规的前端方案(如 localStorage)会失效,因为两个窗口运行在不同进程中。实际做法是让主进程充当数据中转站,通过 IPC 在两个渲染进程之间传递消息。
工作原理
进程隔离与 IPC
两个进程的隔离有明确的安全目的:渲染进程加载的页面可能来自任何地方。如果应用加载了第三方内容,或者用户导入了不受信任的插件页面,渲染进程里运行的恶意代码也拿不到 Node.js 能力——没有 fs 就无法读取用户文件,没有 child_process 就无法执行系统命令。
但这种隔离也带来了实际问题。如果用户点击“选择文件夹”,渲染进程自己无法调出系统对话框。它必须向主进程发送消息,让主进程打开对话框,再把选中的路径传回渲染进程。这就是 IPC 存在的意义——它是渲染进程调用系统能力的唯一通道。
渲染进程通过预加载脚本暴露的接口(如 window.electronAPI)向主进程发送请求,主进程监听对应通道并处理系统调用,再将结果返回给渲染进程。具体的 ipcMain / ipcRenderer 用法将在后续章节展开。
职责分离
两个进程的职责边界很明确,混淆会导致难以排查的问题。
主进程负责的内容:
- 打开文件对话框、保存对话框
- 创建系统菜单和右键菜单
- 操作系统托盘
- 发送桌面通知
- 读取文件系统、执行子进程
- 全局快捷键注册
- 控制窗口:最大化、最小化、置顶、全屏
渲染进程负责的内容:
- HTML 结构和 CSS 样式
- 页面交互逻辑(按钮点击、表单验证)
- DOM 操作
- 通过 Canvas/WebGL 绘制图形
- 网络请求(
fetch、XMLHttpRequest)
基本用法
应用入口脚本
下面是一个能跑起来的最小入口脚本,包含了主进程的典型骨架:生命周期管理、窗口创建、平台差异处理。
js
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
});
win.loadFile(path.join(__dirname, 'index.html'));
// 开发阶段打开开发者工具
win.webContents.openDevTools();
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});nodeIntegration: false 和 contextIsolation: true 是 Electron 12 之后的默认值,显式写出来可以避免依赖隐式行为。activate 事件对应 macOS 的习惯:窗口全关掉以后应用不退出,点 Dock 图标再打开一个新窗口。process.platform 的判断在跨平台应用中几乎必不可少。
创建窗口的时机
创建 BrowserWindow 的时机必须放在 ready 之后,或者等价地放在 app.whenReady().then() 的回调里。如果在模块顶层直接 new BrowserWindow(),会因为 Electron 的基础设施尚未就绪而导致初始化失败。
js
const { app, BrowserWindow } = require('electron');
// ❌ 错误:在 ready 之前创建窗口
// const win = new BrowserWindow({ width: 800, height: 600 });
app.whenReady().then(() => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
});
win.loadFile('index.html');
});注意点
- 主进程中创建的
BrowserWindow实例不要随手挂到全局变量上,否则可能被垃圾回收掉,导致窗口意外消失。 - 渲染进程崩溃不等于主进程崩溃。主进程可以监听
webContents上的crashed事件并重建窗口。 webContents的事件监听在窗口关闭前应当清理,否则会阻止渲染进程正常退出。- macOS 平台的行为与 Windows/Linux 有显著差异,尤其是窗口全部关闭后应用是否退出,
process.platform的判断几乎必须出现。 app.ready在应用启动后只会触发一次。重复创建窗口不是在ready里循环,而是响应其他事件(菜单点击、快捷键、activate等)。
