6月5日 19:54

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:openFileopenFile 更不容易冲突
  • 主进程必须验证输入——渲染进程的代码可能被 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
标签:Electron