Electron 主进程、渲染进程与 preload
🎯 引言
Electron 应用不是把所有代码都放在同一个环境中运行。管理窗口和调用系统能力的代码运行在主进程,页面代码运行在渲染进程,两者之间还可以通过预加载脚本建立安全的通信入口。
本篇只解决一个问题:不同代码应该运行在哪个进程。窗口的详细生命周期会在下一篇之后单独讲解。
学完本篇,你将能够:
- 区分主进程、渲染进程和预加载脚本的职责。
- 理解一个主进程如何管理多个窗口。
- 观察不同窗口拥有独立的页面状态。
- 为下一篇接入 Vue 和后续 IPC 学习建立清晰的进程边界。
🧱 Electron 的进程模型
Electron 应用启动时会先运行 package.json 中 main 指向的文件,这段代码所在的环境就是主进程。一个应用只有一个主进程,它负责管理整个应用。
主进程使用 BrowserWindow 创建窗口。窗口加载 HTML 后,页面代码会在渲染进程中运行。开发时可以把每个窗口看成一个独立的浏览器页面:它们拥有自己的 DOM 和 JavaScript 变量,不会自动共享页面状态。
预加载脚本会在页面脚本之前运行。它负责向渲染进程提供少量、明确的桌面能力,本篇先认识它的位置,具体用法会在 IPC 章节讲解。
| 运行环境 | 主要职责 | 常见内容 |
|---|---|---|
| 主进程 | 管理应用、窗口和系统能力 | app、BrowserWindow |
| 渲染进程 | 显示页面并处理界面交互 | HTML、CSS、DOM、页面脚本 |
| 预加载脚本 | 安全地向页面暴露有限的桌面能力 | contextBridge、受限 IPC API |
nodeIntegration。保留 Electron 的默认隔离设置,再通过预加载脚本暴露必要能力。🛠 创建两个独立的页面环境
下面由一个主进程创建两个窗口。两个窗口加载相同的 HTML 和 JavaScript,但各自保存一份独立的计数器状态。
先创建主进程入口:
const { app, BrowserWindow } = require('electron');
const path = require('node:path');
const windows = new Set();
function createWindow(name) {
const win = new BrowserWindow({
width: 460,
height: 360,
title: name,
});
windows.add(win);
win.loadFile(path.join(__dirname, 'index.html'), {
query: { name },
});
win.on('closed', () => {
windows.delete(win);
});
return win;
}
function createApplicationWindows() {
const firstWindow = createWindow('窗口 A');
const secondWindow = createWindow('窗口 B');
const [x, y] = firstWindow.getPosition();
secondWindow.setPosition(x + 60, y + 60);
}
app.whenReady().then(() => {
createApplicationWindows();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createApplicationWindows();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
windows 保存仍然存在的窗口对象。窗口关闭后将它从集合中移除,避免继续保留已经失效的引用。
两个窗口加载同一个页面,通过查询参数取得自己的名称:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>进程模型示例</title>
</head>
<body>
<h1 id="window-name"></h1>
<p>当前计数:<span id="count">0</span></p>
<button id="add">加一</button>
<script src="./renderer.js"></script>
</body>
</html>
页面交互写在渲染进程中:
const params = new URLSearchParams(window.location.search);
const windowName = params.get('name');
const nameElement = document.querySelector('#window-name');
const countElement = document.querySelector('#count');
const addButton = document.querySelector('#add');
let count = 0;
nameElement.textContent = windowName;
document.title = windowName;
addButton.addEventListener('click', () => {
count += 1;
countElement.textContent = count;
});
运行项目:
npm start
页面会打开“窗口 A”和“窗口 B”。在窗口 A 中连续点击“加一”,窗口 B 的数字不会变化,因为两个窗口拥有独立的 DOM 和 count 变量。
这里不需要 IPC:两个窗口只修改自己的页面状态,没有交换数据。需要在窗口之间共享信息时,再由主进程和 IPC 负责传递。
窗口创建、重新激活和退出属于应用生命周期,下一篇会集中讲解。本篇只保留一个重要前提:BrowserWindow 必须在 app.whenReady() 完成后创建。
💡 代码应该放在哪里
判断代码位置时,先看它操作的对象:
- 创建窗口、退出应用、打开系统对话框,放在主进程。
- 修改文字、监听按钮、更新页面样式,放在渲染进程。
- 页面需要使用桌面能力时,由预加载脚本暴露一个用途明确的接口。
例如,下面这段代码只能放在主进程,因为 BrowserWindow 负责创建桌面窗口:
const win = new BrowserWindow({
width: 600,
height: 400,
});
下面这段代码应该放在渲染进程,因为它操作页面 DOM:
document.querySelector('#title').textContent = '你好,Electron';
渲染进程不能直接调用主进程中的函数。需要跨进程协作时,要通过预加载脚本和 IPC 建立明确的通信通道。
🧾 小节总结
- Electron 应用只有一个主进程,负责管理应用、窗口和系统能力。
BrowserWindow加载的页面运行在独立的页面环境中。- 不同窗口不会自动共享 DOM 和 JavaScript 变量。
- 窗口创建需要等待
app.whenReady(),详细生命周期会在下一篇讲解。 - 预加载脚本负责连接渲染进程与桌面能力,具体通信方式将在下一篇讲解。
❓ 知识问答
Q1:一个 Electron 应用可以创建多个窗口吗?
A:可以。主进程可以创建多个 BrowserWindow,并分别加载不同页面或同一个页面。
Q2:两个窗口中的 JavaScript 变量会自动共享吗?
A:不会。每个窗口都有独立的页面环境,需要共享数据时应通过主进程和 IPC 传递。
Q3:每个窗口一定对应一个独立的操作系统进程吗?
A:不一定。Chromium 会负责底层进程安排。开发时应该依赖窗口之间的页面隔离,不要依赖具体的操作系统进程数量。
Q4:为什么关闭一个窗口后应用没有退出?
A:因为还有其他窗口存在。window-all-closed 只有在所有窗口都关闭后才会触发。
Q5:预加载脚本属于渲染进程吗?
A:它与指定窗口一起运行,但处在和页面隔离的上下文中,拥有比普通页面更高的权限。它应该只暴露页面确实需要的能力。
🧪 小练习
练习一:补全代码,再创建一个名为“窗口 C”的窗口:
function createApplicationWindows() {
createWindow('窗口 A');
createWindow('窗口 B');
// 请在这里编写代码
}
练习二:补全激活事件,在没有窗口时重新创建窗口:
app.on('activate', () => {
// 请在这里编写代码
});
完成练习后,分别修改三个窗口中的计数,观察它们的数字是否会互相影响。
🎉 恭喜你已经理解 Electron 的进程分工啦!下一篇我们会把 Vue 页面接入 Electron,之后再使用 preload 和 IPC 请求主进程能力。
