Electron IPC 通信实战:send、invoke 和 contextBridge 用法对比
Electron 的主进程和渲染进程跑在不同的环境里——一个有 Node.js 权限,一个只有浏览器能力。它们要协作,就得靠 IPC(进程间通信)。理解 IPC 的几种通信模式和它们的取舍,是写好 Electron 应用的基本功。
IPC 通信的三种模式
1. send/on —— 单向通知,不等回复
渲染进程向主进程"喊一声",不等回应:
javascript// 渲染进程 const { ipcRenderer } = require('electron') ipcRenderer.send('log:message', { level: 'info', text: '用户点击了导出按钮' }) // 主进程 const { ipcMain } = require('electron') ipcMain.on('log:message', (event, data) => { logger.log(data.level, data.text) })
主进程也可以主动向渲染进程推消息:
javascript// 主进程 mainWindow.webContents.send('update:progress', { percent: 75 }) // 渲染进程 ipcRenderer.on('update:progress', (event, data) => { progressBar.style.width = data.percent + '%' })
适用场景:日志记录、状态通知、进度更新——发送方不需要知道处理结果。
2. invoke/handle —— 请求-响应,现代写法
渲染进程发起请求,主进程处理后返回结果:
javascript// 渲染进程 const filePath = await ipcRenderer.invoke('dialog:openFile') console.log('选择的文件:', filePath) // 主进程 const { dialog } = require('electron') ipcMain.handle('dialog:openFile', async () => { const result = await dialog.showOpenDialog({ properties: ['openFile'] }) return result.filePaths[0] || null })
invoke 返回 Promise,handle 里可以写异步逻辑。这比老式的 send + event.reply 干净太多,是 Electron 官方推荐的双向通信方式。
为什么比 send/on 好:
- 代码更简洁——不用手动配对 send 和 on
- 天然支持 async/await
- 错误能通过 catch 捕获,不会被静默吞掉
适用场景:需要主进程执行操作并返回结果——打开文件对话框、读写本地文件、调用系统 API。
3. send + event.reply —— 老式双向通信
Electron 7 之前的双向通信方式:
javascript// 渲染进程 ipcRenderer.send('get-file-content', { path: '/data/config.json' }) ipcRenderer.on('file-content-response', (event, data) => { console.log(data) }) // 主进程 ipcMain.on('get-file-content', (event, data) => { const content = fs.readFileSync(data.path, 'utf-8') event.reply('file-content-response', content) })
现在不建议用了——invoke/handle 是更好的替代。event.reply 的主要问题是:渲染进程需要额外监听一个回调通道,代码更散乱,也不支持 async/await。
安全:contextBridge 是必须的
从 Electron 12 开始,contextIsolation 默认开启。这意味着渲染进程的 JavaScript 不能直接 require('electron')——必须通过 preload 脚本和 contextBridge 安全地暴露 API。
javascript// preload.js const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electronAPI', { // 只暴露方法,不暴露 ipcRenderer 对象本身 openFile: () => ipcRenderer.invoke('dialog:openFile'), readFile: (path) => ipcRenderer.invoke('file:read', path), onProgress: (callback) => { const handler = (event, data) => callback(data) ipcRenderer.on('update:progress', handler) return () => ipcRenderer.removeListener('update:progress', handler) } })
javascript// 渲染进程(普通网页环境) const filePath = await window.electronAPI.openFile() const content = await window.electronAPI.readFile(filePath) // 监听进度 const cleanup = window.electronAPI.onProgress((data) => { updateUI(data.percent) }) // 组件卸载时清理 cleanup()
安全原则
- 永远不要把整个 ipcRenderer 暴露给渲染进程——只暴露具体方法
- 通道名用命名空间:
dialog:openFile比openFile更不容易冲突 - 主进程必须验证输入——渲染进程的代码可能被 XSS 篡改,不能信任传来的数据
窗口间通信
多个 BrowserWindow 之间不能直接通信,需要主进程中转:
javascript// 主进程作为消息中转 ipcMain.on('message:forward', (event, data) => { // 转发给所有其他窗口 BrowserWindow.getAllWindows().forEach(win => { if (win.webContents !== event.sender) { win.webContents.send('message:broadcast', data) } }) })
或者用 MessagePort 建立两个渲染进程之间的直接通道:
javascript// 主进程:给两个窗口搭桥 const { MessageChannelMain } = require('electron') ipcMain.on('setup-channel', (event) => { const [port1, port2] = new MessageChannelMain() // port1 给请求方 event.sender.postMessage('channel-established', null, [port1]) // port2 给目标窗口 targetWindow.webContents.postMessage('channel-established', null, [port2]) }) // 渲染进程 A ipcRenderer.on('channel-established', (event) => { const port = event.ports[0] port.postMessage({ type: 'hello', from: 'window-a' }) port.onmessage = (e) => console.log('Received:', e.data) })
MessagePort 的优势是两个窗口直接通信,不经过主进程中转,延迟更低。适合窗口间需要频繁交互的场景(比如编辑器的主窗口和面板窗口)。
IPC 性能优化
批量发送,别逐条发
javascript// 差:1000 次 IPC 调用 items.forEach(item => ipcRenderer.send('process', item)) // 好:1 次 IPC 调用 ipcRenderer.invoke('process-batch', items)
每次 IPC 调用都有序列化和跨进程传输的开销。批量处理能减少调用次数,显著提升性能。
大数据走文件系统或共享内存
IPC 传输大量数据(比如图片、大文件内容)会很慢,因为要经过结构化克隆序列化。正确的做法是:
javascript// 渲染进程请求文件路径,自己读取 const filePath = await ipcRenderer.invoke('file:getPath') // 用 Node.js(preload 暴露的方法)读取文件 const content = await window.electronAPI.readFile(filePath) // 或者用共享内存 const sharedBuffer = await ipcRenderer.invoke('buffer:getShared')
耗时任务用 Worker Threads
不要在主进程里跑 CPU 密集任务——会阻塞所有窗口的 IPC 处理:
javascript// 主进程 const { Worker } = require('worker_threads') ipcMain.handle('heavy-task', async (event, data) => { return new Promise((resolve, reject) => { const worker = new Worker('./heavy-worker.js', { workerData: data }) worker.on('message', resolve) worker.on('error', reject) }) })
TypeScript 类型安全
给 IPC 通道加上类型定义,能在编译时捕获参数错误:
typescript// ipc-types.ts export interface IpcChannels { 'dialog:openFile': { args: void; result: string | null } 'file:read': { args: { path: string }; result: string } 'log:message': { args: { level: string; text: string }; result: void } } // preload.ts contextBridge.exposeInMainWorld('electronAPI', { invoke: <K extends keyof IpcChannels>( channel: K, ...args: IpcChannels[K]['args'] extends void ? [] : [IpcChannels[K]['args']] ): Promise<IpcChannels[K]['result']> => ipcRenderer.invoke(channel, ...args) })
这样渲染进程调用 window.electronAPI.invoke('file:read', { path: 123 }) 时,TypeScript 会报错——path 应该是 string 不是 number。
IPC 通道设计清单
| 检查项 | 说明 |
|---|---|
| 通道名有命名空间 | file:read 而不是 readFile |
| 用 invoke/handle 不用 send/reply | 更安全、更简洁 |
| contextBridge 只暴露方法 | 不暴露 ipcRenderer 对象 |
| 主进程验证输入 | 不信任渲染进程的数据 |
| 大数据不走 IPC | 走文件系统或共享内存 |
| 耗时任务用 Worker | 不阻塞主进程 |
| 监听器记得清理 | 组件卸载时 removeListener |