Electron 的 IPC 和 contextBridge 怎样设计?
说明渲染进程到主进程的单向与双向通信、主进程推送、渲染进程之间的 MessagePort 通信,以及通过 contextBridge 暴露窄接口、处理可序列化数据的设计方法。
参考答案
简答
- 渲染进程需要结果时用
ipcRenderer.invoke配合主进程的ipcMain.handle,返回 Promise;只通知不等结果时用ipcRenderer.send配合ipcMain.on。 - 主进程向渲染进程推送要指定目标窗口,调用
webContents.send,渲染进程用ipcRenderer.on接收;这个方向没有对应的invoke。 - 渲染进程之间没有直接通道,可以由主进程转发,或由主进程分发一对
MessagePort让它们直接通信。 - preload 通过
contextBridge只暴露具体的业务函数,不暴露ipcRenderer本身;监听回调要包一层,避免把event对象传给页面。 - IPC 数据使用结构化克隆算法,DOM 对象、
BrowserWindow等由 C++ 支撑的对象不能传递;sendSync会阻塞渲染进程,官方建议避免使用。
四种通信模式
1. 渲染进程 → 主进程,单向
JavaScript
// preload.js
contextBridge.exposeInMainWorld('api', {
setTitle: (title) => ipcRenderer.send('window:set-title', title),
});
// main.js
ipcMain.on('window:set-title', (event, title) => {
BrowserWindow.fromWebContents(event.sender)?.setTitle(String(title));
});
2. 渲染进程 → 主进程,双向
JavaScript
// preload.js
contextBridge.exposeInMainWorld('api', {
openFile: () => ipcRenderer.invoke('dialog:open-file'),
});
// main.js
ipcMain.handle('dialog:open-file', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({});
return canceled ? null : filePaths[0];
});
invoke 从 Electron 7 开始提供,官方推荐尽量使用它。handler 中抛出的错误会被序列化,渲染进程只能拿到 message,需要错误码时应在返回值里显式约定。
旧写法 send + event.reply 需要另外注册监听器,也没有内置机制把响应和请求对应起来;sendSync 在主进程回复前会阻塞渲染进程。
3. 主进程 → 渲染进程
JavaScript
// main.js
win.webContents.send('menu:zoom', 1);
// preload.js
contextBridge.exposeInMainWorld('api', {
onZoom: (callback) => {
const listener = (_event, value) => callback(value);
ipcRenderer.on('menu:zoom', listener);
return () => ipcRenderer.removeListener('menu:zoom', listener);
},
});
不要写成 ipcRenderer.on('menu:zoom', callback):回调的第一个参数是 event,可以通过 event.sender 拿到 ipcRenderer,等于把整个 IPC 能力交给了页面。返回取消函数可以让组件卸载时移除监听,避免重复注册和内存泄漏。
4. 渲染进程 ↔ 渲染进程
主进程用 MessageChannelMain 创建一对端口,通过 webContents.postMessage 分别发给两个窗口,之后两个窗口直接收发消息,不再经过主进程转发。utility 进程也可以用同样的方式和渲染进程直连。
接口设计建议
- 按业务命名:通道名用
领域:动作的形式(如note:save),前缀只是为了可读,Electron 不会据此做任何处理。 - 窄接口:每个能力一个函数,参数固定,不暴露
invoke(channel, ...args)这种通用转发。 - 集中声明:把通道名、参数和返回值类型放在一个共享的 TypeScript 文件里,主进程、preload 和页面共用,避免拼写不一致。
- 只传可序列化数据:IPC 使用结构化克隆,
Element、process.env、WebContents、BrowserWindow等对象无法传递,函数也不能跨进程调用。传大文件时优先传路径或分块处理。 - 主进程不信任输入:每个 handler 校验发送方和参数,具体做法见《Electron 应用的主进程、渲染进程和 IPC 有哪些安全边界?》。
常见追问
- 为什么不直接用
@electron/remote?它让渲染进程同步调用主进程对象,既有性能问题,也扩大了渲染进程的权限,官方性能指南建议避免使用。 - 多个窗口同时调用同一个 handler 怎么区分?handler 的
event.sender和event.senderFrame标识了调用方,可以据此找到对应窗口或校验来源。进程职责划分见《Electron 的主进程、渲染进程、preload 和 utility 进程分别负责什么?》。