2.3 进程间通信(IPC)原理与实现


2.3 进程间通信(IPC)原理与实现

本节摘要:IPC 是 Electron 主进程与渲染进程之间的官方通信通道,核心 API 是 ipcMain 与 ipcRenderer,现代推荐用 invoke/handle 组成请求响应语义,用 send/on 组成消息推送语义。本节讲两种形态的用法、消息序列化的成本、通道命名与错误处理的设计模式,并展开一个"图片导入进度推送"的完整案例。

两种基本形态

进程之间的对话只有两种形态:问一句等回答(请求响应),和不等你问我就说(消息推送)。Electron 的 API 恰好各配了一对。

请求响应用 invoke 与 handle。页面发起调用,主进程注册处理器,一次调用返回一个 Promise,错误会自动跨进程传播——这是现代代码的默认选择:

// 预加载脚本:把 IPC 包装成受控接口暴露给页面 const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('bridge', { // invoke:请求响应语义,返回 Promise readConfig: (key) => ipcRenderer.invoke('config:get', key), saveConfig: (key, value) => ipcRenderer.invoke('config:set', key, value) });
// 主进程:handle 注册处理器,抛出的异常会传回渲染端的 reject const { app, ipcMain } = require('electron'); const fs = require('node:fs/promises'); const path = require('node:path'); const configPath = () => path.join(app.getPath('userData'), 'config.json'); ipcMain.handle('config:get', async (_event, key) => { const data = JSON.parse(await fs.readFile(configPath(), 'utf8') .catch(() => '{}')); if (!(key in data)) throw new Error(`配置项不存在: ${key}`); return data[key]; }); ipcMain.handle('config:set', async (_event, key, value) => { const data = JSON.parse(await fs.readFile(configPath(), 'utf8') .catch(() => '{}')); data[key] = value; await fs.writeFile(configPath(), JSON.stringify(data, null, 2)); return true; });

消息推送给旧式 API 留了位置:webContents.send 由主进程主动广播,ipcRenderer.on 在页面里订阅。它适合"主进程那边发生了什么,界面需要知道"的场景——下载进度、设备插拔、更新状态。

一条消息的旅程

理解 IPC 的开销模型,比背 API 更重要。调用 invoke 时,参数在渲染进程被结构化克隆(序列化),经进程间管道送到主进程,反序列化后交给处理器;返回值原路再走一遍。这带来三条实践推论:

其一,函数、类实例、DOM 节点不能过桥,能过的只有可被克隆的普通数据。其二,大对象过桥有真实成本,一次传几十兆的 Buffer,序列化与拷贝的时间在低端机上以百毫秒计。其三,每次调用都有固定往返开销,把循环里上千次 invoke 合并成"一次传数组",是 IPC 优化里性价比最高的一招:

// 反模式:循环逐条请求,1000 次往返开销 for (const id of ids) { await window.bridge.readConfig(id); // 慢 } // 正模式:一次请求批处理 const values = await window.bridge.readConfigBatch(ids);

图:一次 invoke 调用的完整旅程

图:一次 invoke 调用的完整旅程

案例:图片导入的进度推送

背景:一个图片管理应用要支持"选文件夹批量导入"。导入由主进程执行(要递归读盘、生成缩略图),但进度条在页面上。这是"请求响应 + 消息推送"混合使用的教科书场景。

操作:页面先 invoke 一个开始导入的命令拿到总任务句柄;主进程干活的间隙用 webContents.send 推进度;页面订阅进度事件更新界面;完成后主进程再推一个完成事件,携带统计结果。

// 主进程:导入 + 双向通信 const { ipcMain, BrowserWindow } = require('electron'); ipcMain.handle('import:start', async (event, dirPath) => { const files = await collectImageFiles(dirPath); // 假设已实现递归收集 const sender = event.sender; // 发消息回渲染进程的句柄 let done = 0; for (const file of files) { await makeThumbnail(file); // 缩略图生成,耗时步骤 done += 1; // 推送进度:节流,每完成 5% 才发一次,避免消息洪峰 if (done % Math.max(1, Math.floor(files.length / 20)) === 0) { sender.send('import:progress', { done, total: files.length }); } } sender.send('import:done', { total: files.length, failed: 0 }); return files.length; });
// 页面侧:命令走 invoke,事件走 on const start = document.getElementById('start'); const bar = document.getElementById('bar'); start.addEventListener('click', async () => { const dir = await window.bridge.pickDirectory(); // 另一个 invoke 命令 const total = await window.bridge.importStart(dir); // 触发导入 console.log('开始导入,共', total, '张'); }); // 订阅推送(预加载脚本里同样要 wrap 后再 expose) bridge.onProgress((p) => { bar.style.width = Math.round((p.done / p.total) * 100) + '%'; });

结果:界面进度条平滑推进,主进程与页面各干各的,互不卡顿。

解读:两个设计决策值得咀嚼——进度用推送而不是让页面轮询 invoke(轮询会产生大量空往返);推送做了节流(千张图片推千次消息,渲染进程光是处理事件就可能掉帧)。变式:如果导入要在应用关闭后仍能续传,就得把任务状态落盘并把执行方搬到独立子进程,IPC 拓扑随之改变——通信设计永远服务于架构,而不是反过来。

通道设计的工程惯例

  • 命名空间 + 动词:config:get、import:start、dialog:open,一眼可读可检索;
  • handle 不做长阻塞:处理器卡住会连累后续同进程请求,长任务应立即返回任务号、结果走推送;
  • 错误显式化:处理器里 throw,让调用端在 catch 里处理,别用返回 null 暗示失败;
  • 渲染端永远只经预加载脚本过桥:不要为了省事在页面里直接摸 ipcRenderer(2.4 节的主题)。

本节要点回顾

  • 两形态:invoke/handle 是请求响应,send/on 是推送,按语义选型;
  • 成本模型:每次 IPC 两次序列化,大对象与高频调用要批量化、节流;
  • 案例要点:命令走 invoke、进度走推送、推送做节流;
  • 通道纪律:命名空间化、快速返回、错误显式传播;
  • 过桥范围:只有可结构化克隆的数据能通信,函数与 DOM 节点止步桥头。

通道修好了,下一节给通道装安检门:上下文隔离与预加载脚本,为什么它们是现代 Electron 的强制配置。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U