目录
[案例4:回复流(一次请求,多次数据 + close)](#案例4:回复流(一次请求,多次数据 + close))
案例1:CPU密集型任务(eg:大量计算、数据处理、加解密、压缩)
案例2:崩溃隔离(eg:第三方插件、不稳定的原生模块、实验性功能)
案例3:渲染进程与效率进程直接通信(MessagePort)
案例1:耗时计算Worker (eg:大量数据处理、复杂计算任务)
[案例2:使用Node.js API的Worker(eg:读取大文件、文件系统操作)](#案例2:使用Node.js API的Worker(eg:读取大文件、文件系统操作))
案例3:定时任务Worker(eg:后台运行定时任务、启动和停止定时器)
[2.5.1、默认沙盒(Electron 20+ 默认行为)](#2.5.1、默认沙盒(Electron 20+ 默认行为))
[2.5.2、禁用沙盒(sandbox: false)](#2.5.2、禁用沙盒(sandbox: false))
2.5.3、全局启用沙盒(app.enableSandbox)
[2.5.4、沙盒环境下的 Preload 脚本演示](#2.5.4、沙盒环境下的 Preload 脚本演示)
一、主进程/渲染进程/preload/IPC入门
官方文档:https://www.electronjs.org/zh/docs/latest/tutorial/installation
应用举例:VScode、钉钉、腾讯会议...
1.1、前期准备
1、安装vs code和node.js(建议v22.14.0),npm对应为v10.9.2;
2、创建项目(二选一)
(1)、创建新项目(会自动安装Electron,快速开始)
npm create @quick-start/electron@latest后进入文件夹,再从第三步继续执行(2)、手动搭建(已有Node项目想添加Electron)
新建文件夹并初始化npm init -y;安装:npm install electron --save-dev,手动创建主进程文件main.js和入口HTML;
3、安装依赖包:npm install;
4、测试运行:npm run start,或自己修改配置文件,增加vite命令,就可以在浏览器运行;
5、打包工具:Electron Builder/Electron Forge/electron-packager等,用于生成安装包与自动更新。打包命令:npm run package(不创建安装程序),npm run make(创建安装程序)。
1.2、名词解释
Electron应用至少有两个「角色」:
主进程 (负责创建窗口、系统API)和渲染进程 (显示用户界面)。
两者不能直接互相调函数,要通过preload预加载脚本 安全地暴露接口,或用IPC (进程间通信)传消息。
主进程:main.js。每个Electron应用有且只有一个主进程。它负责:
(1)、创建和管理应用窗口(渲染进程)
(2)、处理应用程序生命周期(启动、退出、前后台切换)
(3)、与操作系统原生API交互
(4)、管理菜单、对话框等系统级组件
渲染进程:renderer.js。每个Electron窗口都是一个独立的渲染进程。它负责:
(1)、运行在Chromium浏览器环境中
(2)、使用HTML、CSS、JavaScript构建界面
(3)、每个窗口都是独立的进程,互不影响
(4)、通过IPC与主进程通信
预加载脚本:preload.js。预加载脚本是连接主进程和渲染进程的"桥梁"。它负责:
(1)、在渲染进程加载网页之前运行
(2)、有权访问Node.jsAPI和DOM
(3)、通过contextBridge安全地暴露API给渲染进程
IPC通信:由于主进程与渲染进程是独立的,所以必须通过 IPC 传递数据。

常见问题
Q:为什么需要预加载脚本 ?
A:为了安全。渲染进程不能直接访问Node.js API,它作为桥梁,可以安全地暴露必要的API。
Q:`contextBridge `是什么?
A:是个安全API,用于在主进程和渲染进程之间传递数据,避免直接暴露Node.js API。
Q:为什么macOS 关闭窗口后应用不退出?
A:用户体验习惯。关闭窗口后应用仍在Dock中运行,用户可以通过点击Dock图标重新打开窗口。
Q:IPC通信 是同步还是异步 ?
A:本示例使用的是异步通信(`ipcRenderer.invoke()`和`ipcMain.handle()`)。也可以使用同步通信(`ipcRenderer.sendSync()`和`ipcMain.on()`),但不推荐。
1.3、主题切换
用系统主题 nativeTheme或自己切换,和CSS 的prefers-color-scheme配合。当主进程通过 `nativeTheme.themeSource` 改变主题时,浏览器会检测到系统偏好变化,CSS 媒体查询会自动生效,样式就会自动切换。

step1:应用界面index.html
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
<link rel="stylesheet" type="text/css" href="./styles.css">
</head>
<body>
<p>当前主题来源: <strong id="theme-source">System</strong></p>
<button id="toggle-dark-mode">切换模式</button>
<button id="reset-to-system">重置为系统主题</button>
<script src="renderer.js"></script>
</body>
</html>
step2:styles.css
css
:root {
color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
body { background: #333; color: white; }
}
@media (prefers-color-scheme: light) {
body { background: #ddd; color: black; }
}
step3:渲染进程脚本renderer.js
javascript
document
.getElementById("toggle-dark-mode")
.addEventListener("click", async () => {
const isDarkMode = await window.darkMode.toggle();
document.getElementById("theme-source").innerHTML = isDarkMode
? "Dark"
: "Light";
});
document
.getElementById("reset-to-system")
.addEventListener("click", async () => {
await window.darkMode.system();
document.getElementById("theme-source").innerHTML = "System";
});
step4:预加载脚本preload.js
javascript
const { contextBridge, ipcRenderer } = require('electron/renderer')
// 向渲染进程暴露 安全的API
contextBridge.exposeInMainWorld('darkMode', {
toggle: () => ipcRenderer.invoke('dark-mode:toggle'),
system: () => ipcRenderer.invoke('dark-mode:system')
})
step5:主进程main.js
javascript
const { app, BrowserWindow, ipcMain, nativeTheme } = require("electron/main");
const path = require("node:path");
function createWindow() {
// 创建浏览器窗口
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
// 开启开发者工具
devTools: true,
},
});
// 自动打开开发者工具,或者按Ctrl+Shift+I或Cmd+Option+I打开开发者工具
win.webContents.openDevTools();
// 加载应用的 index.html 文件
win.loadFile("index.html");
}
ipcMain.handle("dark-mode:toggle", () => {
if (nativeTheme.shouldUseDarkColors) {
nativeTheme.themeSource = "light";
} else {
nativeTheme.themeSource = "dark";
}
return nativeTheme.shouldUseDarkColors;
});
ipcMain.handle("dark-mode:system", () => {
nativeTheme.themeSource = "system";
});
// 当 Electron 完成初始化并准备创建浏览器窗口时调用此方法。
app.whenReady().then(() => {
createWindow();
// macOS 特殊逻辑:点击 Dock 图标时重新创建窗口
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
// 当所有窗口关闭时触发(仅在 macOS 外生效)
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit(); // 退出应用
}
});
二、进程与通信
*为了快速理解:看完本章后,直接亲手敲一遍"效率进程"的案例1,成功运行后豁然开朗。
2.1、IPC进程间通信
案例1:渲染进程→主进程(单向)

step1:应用界面index.html
html
<body>
<h1>案例 1:渲染进程 → 主进程(单向)</h1>
<p>通过 IPC 把输入框内容设为窗口标题,只发不收。</p>
<label for="title">窗口标题:</label>
<input type="text" id="title" placeholder="输入新标题">
<button type="button" id="btn">设置标题</button>
<p class="hint">通道:set-title,用法:ipcRenderer.send + ipcMain.on</p>
<script src="./renderer.js"></script>
</body>
step2:渲染进程脚本renderer.js
javascript
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value || '未命名'
window.electronAPI.setTitle(title)
})
step3:预加载脚本preload.js
javascript
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
step4:主进程main.js
javascript
// ======================核心代码======================
function handleSetTitle(event, title) {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) win.setTitle(title);
}
app.whenReady().then(() => {
ipcMain.on("set-title", handleSetTitle);
});
案例2:渲染进程→主进程(双向)

step1:应用界面index.html
html
<body>
<h1>案例 2:渲染进程 → 主进程(双向)</h1>
<p>点击按钮打开系统文件选择框,主进程返回选中路径。</p>
<button type="button" id="btn">打开文件</button>
路径:<strong id="filePath">(未选择)</strong>
<p class="hint">通道:dialog:openFile,用法:ipcRenderer.invoke + ipcMain.handle</p>
<script src="./renderer.js"></script>
</body>
step2:渲染进程脚本renderer.js
javascript
const btn = document.getElementById('btn')
const filePathEl = document.getElementById('filePath')
btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathEl.textContent = filePath || '(未选择)'
})
step3:预加载脚本preload.js
javascript
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
step4:主进程main.js
javascript
// ======================核心代码======================
async function handleFileOpen() {
const { canceled, filePaths } = await dialog.showOpenDialog({})
if (!canceled && filePaths.length) return filePaths[0]
return null
}
app.whenReady().then(() => {
ipcMain.handle('dialog:openFile', handleFileOpen)
})
案例3:主进程→渲染进程

step1:应用界面index.html
html
<body>
<h1>案例 3:主进程 → 渲染进程</h1>
<p>通过菜单"+1 / -1"向渲染进程发消息,更新计数;渲染进程可把当前值发回主进程。</p>
<p>当前值:<strong id="counter">0</strong></p>
<p class="hint">通道:update-counter / counter-value</p>
<p>用法:webContents.send + ipcRenderer.on</p>
<script src="./renderer.js"></script>
</body>
step2:渲染进程脚本renderer.js
javascript
const counterEl = document.getElementById('counter')
window.electronAPI.onUpdateCounter((value) => {
const oldValue = Number(counterEl.textContent)
const newValue = oldValue + value
counterEl.textContent = newValue
window.electronAPI.counterValue(newValue)
})
step3:预加载脚本preload.js
javascript
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => {
ipcRenderer.on('update-counter', (_event, value) => callback(value))
},
counterValue: (value) => ipcRenderer.send('counter-value', value)
})
step4:主进程main.js
javascript
// ======================核心代码======================
const menu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{ label: '+1', click: () => mainWindow.webContents.send('update-counter', 1) },
{ label: '-1', click: () => mainWindow.webContents.send('update-counter', -1) }
]
}
])
Menu.setApplicationMenu(menu)
mainWindow.loadFile('index3.html')
ipcMain.on('counter-value', (_event, value) => {
console.log('当前计数:', value)
})
}
| 案例 | 方向 | 主进程 API | 渲染/预加载 API | 是否等回复 |
| 1 | 渲染→主 | `ipcMain.on ` | `ipcRenderer.send` | 否 |
| 2 | 渲染→主 | `ipcMain.handle ` | `ipcRenderer.invoke` | 是(Promise) |
| 3 | 主→渲染 | `webContents.send ` | `ipcRenderer.on` | 可选(再 send 回去) |
2.2、消息端口(MessageChannel)、流式回复
案例1:渲染进程把Port发给主进程

step1:应用界面index.html
html
<body>
<h1>案例 1:渲染进程把 Port 发给主进程</h1>
<p>渲染进程创建 MessageChannel,把 port1 通过 IPC 发给主进程,主进程用 MessagePortMain 收消息并回复。</p>
<button id="send">发一条消息给主进程</button>
<div id="log"></div>
<p class="hint">只有 postMessage 能传输 MessagePort;主进程用 port.on('message')、port.start()。</p>
<script src="./renderer.js"></script>
</body>
step2:渲染进程脚本renderer.js
javascript
const logEl = document.getElementById('log')
const sendBtn = document.getElementById('send')
function log(msg) {
logEl.textContent += new Date().toLocaleTimeString() + ' ' + msg + '\n'
}
window.electronAPI.onMessage((data) => {
log('收到主进程回复: ' + JSON.stringify(data))
})
sendBtn.addEventListener('click', () => {
const payload = { answer: 42, time: Date.now() }
window.electronAPI.postMessage(payload)
log('已发送: ' + JSON.stringify(payload))
})
log('已把 port1 发给主进程,可点击按钮发送消息。')
step3:预加载脚本preload.js
javascript
const { contextBridge, ipcRenderer } = require('electron')
// 在 preload 里创建 MessageChannel 并发送 port1,避免从渲染进程传 port 导致 "Invalid value for transfer"
const channel = new MessageChannel()
const port1 = channel.port1
const port2 = channel.port2
ipcRenderer.postMessage('port', null, [port1])
// contextBridge 不能直接暴露 MessagePort(会丢 postMessage 等原生方法),改为暴露封装函数
contextBridge.exposeInMainWorld('electronAPI', {
postMessage: (data) => port2.postMessage(data),
onMessage: (callback) => {
port2.onmessage = (e) => callback(e.data)
}
})
step4:主进程main.js
javascript
// ======================核心代码======================
app.whenReady().then(() => {
// 渲染进程通过 postMessage 把 MessagePort 发过来(只有 postMessage 能传 port)
ipcMain.on('port', (event) => {
const port = event.ports[0]
// 主进程里变成 MessagePortMain,用 Node 风格 .on('message', ...)
port.on('message', (e) => {
console.log('主进程收到:', e.data)
port.postMessage({ received: true, echo: e.data })
})
port.start()
})
})
功能:
渲染进程创建 `MessageChannel`,把 port1 通过 `ipcRenderer.postMessage('port', null, [port1])` 发给主进程;主进程用 `event.ports[0]` 拿到 **MessagePortMain**,`port.on('message', ...)` 收消息、`port.postMessage(...)` 回复,最后 `port.start()`。
要点:(1)、只有 **postMessage** 能传 port,send/invoke 不行。
(2)、主进程里是 **MessagePortMain**,用 `.on('message', (e) => e.data)`、`.start()`。
案例2:两个渲染进程通过主进程建立通道

step1:应用界面index.html

step2:渲染进程脚本renderer.js

step3:预加载脚本preload.js
javascript
const { contextBridge, ipcRenderer } = require('electron')
// 不能把 port 直接传给渲染进程(contextBridge 会丢原生方法),在 preload 里持有 port 并暴露封装函数
let port = null
let messageCallback = null
ipcRenderer.on('port', (event) => {
port = event.ports[0]
if (messageCallback) port.onmessage = (ev) => messageCallback(ev.data)
})
contextBridge.exposeInMainWorld('electronAPI', {
postMessage: (data) => {
if (port) port.postMessage(data)
},
onMessage: (callback) => {
messageCallback = callback
if (port) port.onmessage = (ev) => callback(ev.data)
}
})
step4:主进程main.js
javascript
app.whenReady().then(async () => {
const mainWindow = new BrowserWindow({
width: 520,
height: 400,
show: false,
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, 'preload2.js')
}
})
mainWindow.loadFile('index2a.html')
const secondaryWindow = new BrowserWindow({
width: 520,
height: 400,
show: false,
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, 'preload2.js')
}
})
secondaryWindow.loadFile('index2b.html')
const { port1, port2 } = new MessageChannelMain()
mainWindow.once('ready-to-show', () => {
mainWindow.show()
mainWindow.webContents.postMessage('port', null, [port1])
})
secondaryWindow.once('ready-to-show', () => {
secondaryWindow.show()
secondaryWindow.webContents.postMessage('port', null, [port2])
})
})
功能:
主进程创建 **MessageChannelMain**,在 `ready-to-show` 后把 port1 发给窗口 A、port2 发给窗口 B;两窗口通过 preload 收到 port 后,用 `port.postMessage` / `port.onmessage` 直接通信,主进程不再参与。
要点:(1)、主进程用 `new MessageChannelMain()` 得到 `port1`、`port2`。
(2)、用 `webContents.postMessage('port', null, [port1])` 把端口发给对应窗口。
(3)、预加载里 `ipcRenderer.on('port', (event) => event.ports[0])`,再通过 contextBridge 把 port 交给页面。
案例3:主窗口与Worker窗口直连

step1:应用界面index.html
html
<body>
<h1>案例 3:主窗口与 Worker 窗口直连</h1>
<p>Worker 是隐藏窗口,主窗口通过主进程拿到与 Worker 的 MessagePort,直接通信(主进程不转发内容)。</p>
<input type="number" id="num" value="21">
<button id="send">发给 Worker 计算 (×2)</button>
<div id="log"></div>
<p class="hint">主进程只负责建立 channel 并分发 port,不参与后续消息内容。</p>
<script src="./renderer3.js"></script>
</body>
html
<!-- worker.html 隐藏 -->
<body>
<p>Worker 窗口(隐藏),收到数字后 ×2 并回复。</p>
<script src="./renderer3-worker.js"></script>
</body>
step2:渲染进程脚本renderer.js
javascript
const logEl = document.getElementById('log')
const sendBtn = document.getElementById('send')
const numInput = document.getElementById('num')
function log(msg) {
logEl.textContent += new Date().toLocaleTimeString() + ' ' + msg + '\n'
}
window.electronAPI.requestWorkerChannel()
window.electronAPI.onMessage((data) => {
log('Worker 结果: ' + data)
})
log('已连接 Worker')
sendBtn.addEventListener('click', () => {
const n = Number(numInput.value) || 0
window.electronAPI.postMessage(n)
log('已发送: ' + n)
})
javascript
// Worker 窗口:收到数字后 ×2 并 postMessage 回去
window.electronAPI.onMessage((data) => {
const result = data * 2
window.electronAPI.postMessage(result)
})
step3:预加载脚本preload.js
javascript
const { contextBridge, ipcRenderer } = require('electron')
// 不能把 port 直接传给渲染进程(会丢 postMessage 等),在 preload 里持有 port 并暴露封装函数
let port = null
let messageCallback = null
ipcRenderer.on('provide-worker-channel', (event) => {
port = event.ports[0]
if (messageCallback) port.onmessage = (ev) => messageCallback(ev.data)
})
contextBridge.exposeInMainWorld('electronAPI', {
requestWorkerChannel: () => ipcRenderer.send('request-worker-channel'),
postMessage: (data) => {
if (port) port.postMessage(data)
},
onMessage: (callback) => {
messageCallback = callback
if (port) port.onmessage = (ev) => callback(ev.data)
}
})
javascript
const { contextBridge, ipcRenderer } = require('electron')
// 不能把 port 直接传给渲染进程(会丢 postMessage 等),在 preload 里持有 port 并暴露封装函数
let port = null
let messageCallback = null
ipcRenderer.on('new-client', (event) => {
port = event.ports[0]
if (messageCallback) port.onmessage = (ev) => messageCallback(ev.data)
})
contextBridge.exposeInMainWorld('electronAPI', {
postMessage: (data) => {
if (port) port.postMessage(data)
},
onMessage: (callback) => {
messageCallback = callback
if (port) port.onmessage = (ev) => callback(ev.data)
}
})
step4:主进程main.js
javascript
// ==============核心代码===================
const mainWindow = new BrowserWindow({
width: 520,
height: 420,
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, 'preload3-app.js')
}
})
mainWindow.loadFile('index3.html')
// 主窗口请求 worker 通道时,建立 MessageChannel,一端给 worker,一端给主窗口
ipcMain.on('request-worker-channel', (event) => {
const { port1, port2 } = new MessageChannelMain()
workerWindow.webContents.postMessage('new-client', null, [port1])
event.sender.postMessage('provide-worker-channel', null, [port2])
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
功能:
Worker 是一个隐藏的 BrowserWindow。主窗口通过 IPC 向主进程请求"worker 通道";主进程建立 **MessageChannelMain**,一端发给 Worker 窗口(`new-client`),一端发回主窗口(`provide-worker-channel`)。之后主窗口和 Worker 直接通过 port 通信,主进程不转发消息内容。
要点:(1)、主进程监听 `request-worker-channel`,创建 channel,`port1` 给 worker、`port2` 给 `event.sender`(主窗口)。
(2)、Worker 用 `ipcRenderer.on('new-client', ...)` 收 port,主窗口用 `ipcRenderer.on('provide-worker-channel', ...)` 收 port。
案例4:回复流(一次请求,多次数据 + close)

step1:应用界面index.html
html
<body>
<p>一次请求,主进程通过 port 连续发送多条数据后 close,实现"流式"回复。</p>
<label>数值</label><input type="number" id="element" value="42">
<label>条数</label><input type="number" id="count" value="10">
<button id="btn">请求流</button>
<div id="log"></div>
<p class="hint">通道:give-me-a-stream;主进程 for 循环 postMessage 后 replyPort.close()。</p>
<script src="./renderer.js"></script>
</body>
step2:渲染进程脚本renderer.js
javascript
const logEl = document.getElementById('log')
const btn = document.getElementById('btn')
const elementInput = document.getElementById('element')
const countInput = document.getElementById('count')
function log(msg) {
logEl.textContent += new Date().toLocaleTimeString() + ' ' + msg + '\n'
}
btn.addEventListener('click', () => {
const element = Number(elementInput.value) || 42
const count = Number(countInput.value) || 10
logEl.textContent = ''
window.electronAPI.requestStream(
{ element, count },
(data) => log('收到: ' + data),
() => log('流结束')
)
log('已发送请求,等待主进程流式回复...')
})
step3:预加载脚本preload.js
javascript
const { contextBridge, ipcRenderer } = require('electron')
// 在 preload 里创建 MessageChannel 并发送 port2,避免从渲染进程传 port 导致 "Invalid value for transfer"
contextBridge.exposeInMainWorld('electronAPI', {
requestStream: (payload, onData, onClose) => {
const { port1, port2 } = new MessageChannel()
port1.onmessage = (e) => onData(e.data)
port1.onclose = () => onClose && onClose()
port1.start()
ipcRenderer.postMessage('give-me-a-stream', payload, [port2])
}
})
step4:主进程main.js
javascript
app.whenReady().then(() => {
// 回复流:渲染进程发一个 port 过来,主进程连续 postMessage 多次后 close
ipcMain.on('give-me-a-stream', (event, msg) => {
const [replyPort] = event.ports
for (let i = 0; i < msg.count; i++) {
replyPort.postMessage(msg.element)
}
replyPort.close()
})
})
功能:
渲染进程创建 MessageChannel,把 port2 和参数(如 `{ element, count }`)通过 `postMessage('give-me-a-stream', msg, [port2])` 发给主进程。主进程按 `count` 次 `replyPort.postMessage(element)`,最后 `replyPort.close()`。渲染进程在 port1 上多次收数据,并在 `port1.onclose` 得知流结束。
要点:(1)、普通 IPC 只有"发一条 / 回一条"或 invoke 一次回一次;用 port 可以实现"一条请求、多条回复、再关闭"。
(2)、主进程发完后 **replyPort.close()**,渲染进程会触发 **close** 事件。
2.3、效率进程(utility.js)
主进程 :负责创建窗口、响应系统事件,必须保持不卡、不崩。(案例1是全部代码,其余是是核心代码)。
效率进程(utility.js) :独立子进程,可访问 Node.js API,专门干重活或容易崩的活。效率进程必须用 process.parentPort 收发消息,不能用 process.send / process.on('message')
案例1:CPU密集型任务(eg:大量计算、数据处理、加解密、压缩)

Step1:主进程main.js
javascript
const { app, BrowserWindow, utilityProcess: forkUtilityProcess } = require('electron/main')
const path = require('node:path')
let utilityProcess = null
function createWindow() {
const mainWindow = new BrowserWindow({
width: 1000,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index2.html')
// 等页面加载完成后再创建效率进程,避免渲染进程错过 spawn/ready 消息
mainWindow.webContents.once('did-finish-load', () => {
utilityProcess = forkUtilityProcess.fork(path.join(__dirname, 'utility.js'))
setupUtilityListeners(mainWindow)
})
// 处理来自渲染进程的请求
const { ipcMain } = require('electron/main')
ipcMain.on('start-task', (event, taskData) => {
if (utilityProcess && !utilityProcess.killed) {
utilityProcess.postMessage({
type: 'start-task',
data: taskData
})
}
})
}
function setupUtilityListeners(mainWindow) {
utilityProcess.on('message', (message) => {
mainWindow.webContents.send('utility-message', message)
})
utilityProcess.on('exit', (code) => {
console.log('效率进程已退出,退出码:', code)
mainWindow.webContents.send('utility-exit', code)
utilityProcess = null
})
utilityProcess.on('spawn', () => {
console.log('✅ 效率进程已启动')
mainWindow.webContents.send('utility-spawn')
})
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => {
if (utilityProcess && !utilityProcess.killed) {
utilityProcess.kill()
}
if (process.platform !== 'darwin') {
app.quit()
}
})
Step2:预加载脚本preload.js
javascript
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
startTask: (taskData) => {
ipcRenderer.send('start-task', taskData)
},
onUtilityMessage: (callback) => {
ipcRenderer.on('utility-message', (event, message) => {
callback(message)
})
},
onUtilityExit: (callback) => {
ipcRenderer.on('utility-exit', (event, code) => {
callback(code)
})
},
onUtilitySpawn: (callback) => {
ipcRenderer.on('utility-spawn', () => {
callback()
})
}
})
Step3:渲染进程脚本renderer.js
javascript
const taskSelect = document.getElementById('taskSelect')
const valueInput = document.getElementById('valueInput')
const startBtn = document.getElementById('startBtn')
const logArea = document.getElementById('logArea')
const statusText = document.getElementById('statusText')
const progressBar = document.getElementById('progressBar')
let taskIdCounter = 0
function addLog(message, type = 'info') {
const time = new Date().toLocaleTimeString()
const logEntry = document.createElement('div')
logEntry.className = `log-entry ${type}`
logEntry.textContent = `[${time}] ${message}`
logArea.appendChild(logEntry)
logArea.scrollTop = logArea.scrollHeight
}
function updateProgress(percent) {
progressBar.style.width = `${percent}%`
progressBar.textContent = `${percent}%`
}
// 监听效率进程消息
window.electronAPI.onUtilityMessage((message) => {
if (message.type === 'task-started') {
addLog(`任务 ${message.taskId} 已开始`, 'info')
updateProgress(50)
} else if (message.type === 'task-completed') {
addLog(`任务 ${message.taskId} 已完成`, 'success')
addLog(`结果: ${JSON.stringify(message.result, null, 2)}`, 'success')
updateProgress(100)
setTimeout(() => updateProgress(0), 1000)
} else if (message.type === 'ready') {
addLog(message.message, 'success')
statusText.textContent = '就绪'
statusText.style.color = '#4CAF50'
}
})
// 监听效率进程退出
window.electronAPI.onUtilityExit((code) => {
addLog(`效率进程已退出,退出码: ${code}`, 'warning')
statusText.textContent = '已停止'
statusText.style.color = '#dc3545'
})
// 监听效率进程启动
window.electronAPI.onUtilitySpawn(() => {
addLog('效率进程已启动', 'success')
statusText.textContent = '运行中'
statusText.style.color = '#4CAF50'
})
// 开始任务
startBtn.addEventListener('click', () => {
const taskType = taskSelect.value
const value = parseInt(valueInput.value) || 100
if (!taskType) {
addLog('请选择任务类型', 'warning')
return
}
const taskId = ++taskIdCounter
const taskData = {
taskId: taskId,
taskType: taskType,
value: value
}
addLog(`开始任务 ${taskId}: ${taskType} (值: ${value})`, 'info')
updateProgress(10)
window.electronAPI.startTask(taskData)
})
// 初始化
addLog('等待效率进程启动...', 'info')
Step4:应用界面index.html
html
<style>
.progress-bar {
width: 100%;
height: 20px;
background-color: #eee;
border-radius: 10px;
overflow: hidden;
position: relative;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #4a90e2, #357adb);
width: 0%;
transition: width 0.3s;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
}
.log-area {
background: #2d2d2d;
color: #f8f8f2;
padding: 15px;
border-radius: 4px;
height: 300px;
overflow-y: auto;
font-size: 14px;
}
.log-entry {
margin-bottom: 8px;
line-height: 1.6;
padding: 5px;
border-radius: 3px;
}
.log-entry.info {
background: orange;
}
.log-entry.success {
background: green;
}
.log-entry.warning {
background: red;
}
</style>
<div class="status-bar">
<span class="status-label">效率进程状态:</span>
<span class="status-value" id="statusText">启动中...</span>
</div>
<div class="task-controls">
<div class="control-group">
<label>任务类型:</label>
<select id="taskSelect">
<option value="">请选择任务</option>
<option value="fibonacci">斐波那契数列(计算第 N 项)</option>
<option value="prime">查找质数(查找 1 到 N 的所有质数)</option>
<option value="sum">大数求和(计算 1 到 N 的和)</option>
</select>
</div>
<div class="control-group">
<label>数值:</label>
<input type="number" id="valueInput" value="100" min="1" max="1000000">
</div>
<div class="control-group">
<button class="btn" id="startBtn">开始任务</button>
</div>
</div>
<div class="progress-container">
<div class="progress-bar">
<div class="progress-bar-fill" id="progressBar">0%</div>
</div>
</div>
<div>
<h3 style="margin-bottom: 15px; color: #333; font-size: 18px;">📝 任务日志</h3>
<div class="log-area" id="logArea"></div>
</div>
Step5:效率进程utility.js
javascript
// 监听来自主进程的消息(Electron 使用 parentPort)
process.parentPort.on('message', (e) => {
const message = e.data
if (message.type === 'start-task') {
const taskData = message.data
process.parentPort.postMessage({
type: 'task-started',
taskId: taskData.taskId
})
const result = performHeavyTask(taskData)
process.parentPort.postMessage({
type: 'task-completed',
taskId: taskData.taskId,
result: result
})
}
})
// CPU 密集型任务:计算斐波那契数列
function performHeavyTask(taskData) {
const { taskType, value } = taskData
if (taskType === 'fibonacci') {
return calculateFibonacci(value)
} else if (taskType === 'prime') {
return findPrimes(value)
} else if (taskType === 'sum') {
return calculateSum(value)
}
return { error: 'Unknown task type' }
}
// 计算斐波那契数列(CPU 密集型)
function calculateFibonacci(n) {
if (n <= 1) return n
let a = 0, b = 1
for (let i = 2; i <= n; i++) {
const temp = a + b
a = b
b = temp
}
return {
n: n,
result: b,
message: `斐波那契数列第 ${n} 项: ${b}`
}
}
// 查找质数(CPU 密集型)
function findPrimes(max) {
const primes = []
const startTime = Date.now()
for (let num = 2; num <= max; num++) {
let isPrime = true
for (let i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) {
isPrime = false
break
}
}
if (isPrime) {
primes.push(num)
}
const endTime = Date.now()
return {
max: max,
count: primes.length,
primes: primes.slice(0, 10), // 只返回前10个
time: endTime - startTime,
message: `找到 ${primes.length} 个质数(耗时 ${endTime - startTime}ms)`
}
}
// 计算大数求和(CPU 密集型)
function calculateSum(max) {
let sum = 0
const startTime = Date.now()
for (let i = 1; i <= max; i++) {
sum += i
}
const endTime = Date.now()
return {
max: max,
sum: sum,
time: endTime - startTime,
message: `1 到 ${max} 的和: ${sum}(耗时 ${endTime - startTime}ms)`
}
}
// 发送就绪消息
process.parentPort.postMessage({
type: 'ready',
message: '效率进程已就绪,可以处理 CPU 密集型任务',
pid: process.pid
})
process.on('exit', (code) => {
console.log('效率进程退出,退出码:', code)
})
案例2:崩溃隔离(eg:第三方插件、不稳定的原生模块、实验性功能)
Step1:主进程main.js
javascript
// 主进程监听退出
utilityProcess.on('exit', (code) => {
mainWindow.webContents.send('utility-exit', code)
if (code !== 0) {
mainWindow.webContents.send('utility-crashed', code)
}
utilityProcess = null
})
// 崩溃后重启
ipcMain.on('restart-utility', () => {
if (utilityProcess && !utilityProcess.killed) {
utilityProcess.kill()
}
utilityProcess = forkUtilityProcess.fork(path.join(__dirname, 'utility3.js'))
setupUtilityListeners(mainWindow)
})
案例3:渲染进程与效率进程直接通信(MessagePort)
Step1:主进程main.js
javascript
// 主进程创建通道并分发端口
const { port1, port2 } = new MessageChannelMain()
utilityProcess.on('spawn', () => {
utilityProcess.postMessage({ port: port2 }, [port2])
mainWindow.webContents.postMessage('main-port', null, [port1])
})
Step2:渲染进程脚本renderer.js
javascript
// 效率进程接收 port 并通信
ipcRenderer.on('main-port', (event) => {
rendererPort = event.ports[0]
rendererPort.on('message', (event) => {
window.dispatchEvent(new CustomEvent('utility-message', { detail: event.data }))
})
rendererPort.start()
})
Step3:效率进程utility.js
javascript
// 效率进程接收 port 并通信
process.on('message', (message, transferList) => {
if (message.port) {
rendererPort = message.port
rendererPort.on('message', (event) => {
rendererPort.postMessage({ type: 'response', data: '...' })
})
rendererPort.start()
}
})
常见问题
Q:效率进程 和Web Worker 有什么区别 ?
A: 前者是独立进程,可用完整 Node.js,崩溃不影响主进程;后者在渲染进程内,通常无 Node。
Q:效率进程崩溃 后如何恢复 ?
A:在主进程监听 `exit`,若 `code !== 0` 可视为异常退出,再 `new UtilityProcess(...)` 创建新进程并重新绑定监听即可。
Q:渲染进程如何与效率进程直接通信 ?
A:主进程只负责创建 `MessageChannel`,把 `port1` 交给渲染进程、`port2` 交给效率进程,之后不参与消息内容转发。渲染进程通过 preload 暴露的 API 使用 port 收发消息。
Q:效率进程里可以用 npm 包 吗?
A:可以,效率进程是标准 Node 环境,可以 `require()` 任意已安装的模块。
2.4、多线程(Worker线程)
问题:如果让主线程执行耗时任务(比如计算1亿次循环),界面会卡住,用户点击按钮没反应。
解决:把耗时任务交给 Worker 线程,主线程继续工作,界面保持流畅。
案例1:耗时计算Worker (eg:大量数据处理、复杂计算任务)
Step1:主进程main.js
javascript
nodeIntegrationInWorker: true // 启用 Worker 中的 Node.js 支持
Step2:渲染进程脚本renderer.js
javascript
let worker1 = null;
const calcResult = document.getElementById("calc-result");
const startCalcBtn = document.getElementById("start-calc");
const stopCalcBtn = document.getElementById("stop-calc");
startCalcBtn.addEventListener("click", () => {
if (worker1) {
worker1.terminate();
}
const iterations =
parseInt(document.getElementById("iterations").value) || 100000000;
calcResult.textContent = "正在计算...";
calcResult.classList.remove("empty");
startCalcBtn.disabled = true;
stopCalcBtn.disabled = false;
// 创建 Worker
worker1 = new Worker("worker.js");
// 监听 Worker 消息
worker1.onmessage = (event) => {
const { type, data } = event.data;
if (type === "result") {
calcResult.textContent =
`计算完成!\n` +
`迭代次数: ${data.iterations}\n` +
`计算结果: ${data.sum}\n` +
`耗时: ${data.duration}ms`;
startCalcBtn.disabled = false;
stopCalcBtn.disabled = true;
worker1.terminate();
worker1 = null;
}
};
// 发送计算任务
worker1.postMessage({ type: "calculate", data: iterations });
});
Step3:worker.js
javascript
// 监听主线程发送的消息
self.onmessage = function (event) {
const { type, data } = event.data
if (type === 'calculate') {
// 执行耗时计算
const result = performHeavyCalculation(data)
// 发送结果回主线程
self.postMessage({ type: 'result', data: result })
}
}
// 耗时计算函数
function performHeavyCalculation(input){
let sum=0
const iterations = input || 100000000 // 1亿次循环
const startTime = Date.now()
for (let i = 0; i < iterations; i++) {
sum += i
}
const endTime= Date.now()
const duration = endTime - startTime
console.log('Worker 1 计算完成,耗时:', duration, 'ms')
return {
sum,
iterations,
duration
}
}
Step4:应用界面index.html
html
<div class="demo-section">
<h2>耗时计算</h2>
<div>
<label>迭代次数:</label>
<input type="number" id="iterations" value="1000000" min="1000000" max="1000000000" />
</div>
<div class="button-group">
<button id="start-calc">开始计算</button>
<button id="stop-calc" disabled>停止计算</button>
</div>
<div class="result-box empty" id="calc-result">等待计算结果...</div>
</div>
<script src="./renderer.js"></script>
案例2:使用Node.js API的Worker(eg:读取大文件、文件系统操作)
渲染进程renderer.js
javascript
let worker2 = null;
const nodeResult = document.getElementById("node-result");
const readFileBtn = document.getElementById("read-file");
const listDirBtn = document.getElementById("list-dir");
function createWorker2() {
if (worker2) {
worker2.terminate();
}
worker2 = new Worker("worker.js");
worker2.onmessage = (event) => {
const { type, data } = event.data;
if (type === "fileContent") {
nodeResult.textContent = `文件路径:${data.path}\n` +
`文件大小:${data.size} 字节\n` +
`内容预览(前1000字符):\n${data.content}`;
} else if (type === 'dirList') {
nodeResult.textContent = `目录路径:${data.path}\n` +
`文件列表(前20个):\n${data.files.join('\n')}`
} else if (type === 'error') {
nodeResult.textContent = `错误:${data}`
}
nodeResult.classList.remove('empty')
};
}
readFileBtn.addEventListener('click', () => {
createWorker2()
nodeResult.textContent = '正在读取文件...'
nodeResult.classList.remove('empty')
// 读取package.json
worker2.postMessage({
type: 'readFile',
path: { path: './package.json' }
});
})
listDirBtn.addEventListener('click', () => {
createWorker2()
nodeResult.textContent = '正在列出目录...'
nodeResult.classList.remove('empty')
// 列出当前目录
worker2.postMessage({
type: 'listDir',
path: { path: '.' }
});
})
worker.js
javascript
const fs = require('fs')
// 监听主线程发送的消息
self.onmessage = function (event) {
const { type, path } = event.data
if (type === 'readFile') {
try {
const filePath = path.path
const content = fs.readFileSync(filePath, 'utf-8')
self.postMessage({
type: 'fileContent',
data: {
path: filePath,
content: content.substring(0, 1000),
size: content.length
}
})
} catch (error) {
self.postMessage({
type: 'error',
data: error.message
})
}
} else if (type === 'listDir') {
try {
const dirPath = path.path
const files = fs.readdirSync(dirPath)
self.postMessage({
type: 'dirList',
data: {
path: dirPath,
files: files.slice(0, 20)
}
})
} catch (error) {
self.postMessage({
type: 'error',
data: error.message
})
}
}
}
案例3:定时任务Worker(eg:后台运行定时任务、启动和停止定时器)

渲染进程renderer.js
javascript
let worker3 = null
const timerResult = document.getElementById('timer-result')
const timerStatus = document.getElementById('timer-status')
const startTimerBtn = document.getElementById('start-timer')
const stopTimerBtn = document.getElementById('stop-timer')
startTimerBtn.addEventListener('click', function () {
if (worker3) {
worker3.terminate()
}
const interval = parseInt(document.getElementById('timer-interval').value)
timerResult.textContent = '定时器启动中...'
timerResult.classList.remove('empty')
timerStatus.textContent = '运行中'
timerStatus.className = 'running'
startTimerBtn.disabled = true
stopTimerBtn.disabled = false
worker3 = new Worker('worker3.js')
worker3.onmessage = (event) => {
const { type, data } = event.data
if (type === 'timerUpdate') {
timerResult.textContent = `计数器:${data.counter}\n` +
`时间:${data.timestamp}`
} else if (type === 'timerStarted') {
timerResult.textContent = `定时器已启动,间隔:${data.interval}毫秒`
} else if (type === 'timerStopped') {
timerResult.textContent = `定时器已停止\n最终计数:${data.finalCount}`
timerStatus.textContent = '已停止'
timerStatus.className = 'stopped'
startTimerBtn.disabled = false
stopTimerBtn.disabled = true
worker3.terminate()
worker3 = null
}
}
// 启动定时器
worker3.postMessage({
type: 'start',
data: { interval }
})
})
stopTimerBtn.addEventListener('click', () => {
if (worker3) {
worker3.postMessage({
type: 'stopTimer'
})
}
})
worker.js
javascript
let intervalId = null
let counter = 0
// 监听主线程发送的消息
self.onmessage = function (event) {
const { type, data } = event.data
if (type === 'start') {
// 启动定时器
const interval = data.interval || 1000
if (intervalId) {
clearInterval(intervalId)
}
counter = 0
intervalId = setInterval(() => {
counter++
// 定期发送状态
self.postMessage({
type: 'timerUpdate',
data: {
counter,
timestamp: new Date().toLocaleTimeString()
}
})
}, interval)
self.postMessage({
type: 'timerStarted',
data: {
interval
}
})
} else if (type === 'stopTimer') {
// 停止计时器
if (intervalId) {
clearInterval(intervalId)
intervalId = null
}
self.postMessage({
type: 'timerStopped',
data: { finalCount: counter }
})
}
}
2.5、进程沙盒(sandbox)
沙盒化:渲染进程被限制在安全环境中,不能直接访问 Node.js API 和大部分系统资源。
安全优势:即使渲染进程被注入恶意代码,也无法直接操作文件系统/执行系统命令等危险操作。
通信方式:沙盒化的渲染进程只能通过 IPC 与主进程通信,由主进程代为执行需要权限的操作。
2.5.1、默认沙盒(Electron 20+ 默认行为)
特点
(1)、不设置 `sandbox` 选项,使用默认值(true)
(2)、渲染进程无法直接使用 `require('fs')` 等 Node.js API
(3)、通过 preload 脚本暴露安全的 API
(4)、通过 IPC 调用主进程的文件操作
2.5.2、禁用沙盒(sandbox: false)
特点
(1)、通过设置 sandbox: false 可以禁用单个窗口的沙盒
(2)、禁用沙盒后,渲染进程仍然没有 Node.js 环境(除非同时设置 nodeIntegration: true )
(3)、不推荐在生产环境禁用沙盒,这会带来安全风险
(4)、如果必须禁用沙盒,确保只加载可信内容
2.5.3、全局启用沙盒(app.enableSandbox)
特点
(1)、app.enableSandbox() 必须在应用的 ready 事件之前调用
(2)、调用后,所有窗口都会被强制沙盒化,即使设置了 sandbox: false
(3)、这确保了应用的所有渲染进程都处于沙盒环境中
(4)、适用于需要强制所有窗口都启用沙盒的场景
javascript
// ⚠️ 重要:必须在 app.whenReady() 之前调用
app.enableSandbox()
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// 即使这里设置 sandbox: false,也会被 app.enableSandbox() 覆盖
sandbox: false, // 这个设置会被 app.enableSandbox() 覆盖
preload: path.join(__dirname, 'preload3.js'),
contextIsolation: true
}
})
2.5.4、沙盒环境下的 Preload 脚本演示
特点
(1)、在沙盒环境下,preload 脚本可以使用有限的 Node.js API
(2)、**可用模块:**electron(部分)、events、timers、url
(3)、**可用全局对象:**Buffer、process、clearImmediate、setImmediate
(4)、不能使用:fs、path、child_process等大部分 Node.js 模块
(5)、需要通过 IPC 与主进程通信来执行需要权限的操作

