Electron IPC 模式
Electron IPC 的模式选择与安全边界:renderer→main 单向消息、invoke/handle 请求响应、main→renderer 推送分别解决不同通信需求;所有高权限 IPC 都应经 preload/contextBridge 暴露窄能力并在 main 验证 sender 与参数。
[!info] related notes
- 所属 MOC: Electron MOC
- IPC 基础: IPC
- Preload: Electron preload 脚本
- Bridge: Electron contextBridge
- Isolation: Electron contextIsolation
- Process: Electron 主进程, Electron 渲染进程
- 安全: Electron 安全检查清单
Electron IPC 模式
一句话关系
Electron IPC 不是一个万能 RPC API。
常见通信需求至少分成三类:
renderer → main one-way
renderer → main request/response
main → renderer event/push
每类应该使用不同模式,并通过 preload 暴露固定 capability。
为什么需要 IPC
Electron 的 main 与 renderer 承担不同职责。
Renderer:
- UI;
- DOM;
- Web APIs;
- 低权限页面逻辑。
Main:
- BrowserWindow;
- filesystem / OS integration;
- native dialog;
- app lifecycle;
- privileged operations。
因此 renderer 需要高权限能力时,不应直接拥有该能力,而应:
renderer
→ preload API
→ IPC
→ main
→ privileged operation
Pattern 1:Renderer → Main 单向消息
典型 API:
ipcRenderer.send
→ ipcMain.on
适合:
- fire-and-forget;
- analytics/event;
- 无返回值 command;
- UI 通知 main。
例如:
setWindowTitle(title)
如果 renderer 不需要 result,就没必要构造 request/response。
Pattern 2:Renderer → Main 请求响应
典型 API:
ipcRenderer.invoke
→ ipcMain.handle
它返回 Promise,适合:
- 打开文件对话框;
- 读取配置;
- 获取 app version;
- 执行有结果的 privileged command。
例如:
window.appApi.openFile()
→ invoke fixed channel
→ main handle
→ return selected file metadata
对于 request/response,这是当前更自然的默认选择。
官方 Electron IPC 教程也优先使用 invoke/handle,而不是手工维护 request-id + reply channel。
Pattern 3:Main → Renderer 推送
典型 API:
webContents.send
→ ipcRenderer.on
适合:
- update available;
- native menu action;
- OS event;
- main process state change。
Main 必须明确发送给哪个 WebContents。
它不是全局 broadcast bus。
sendSync
ipcRenderer.sendSync 会阻塞 renderer,直到 main 返回。
这会:
- 卡住 UI;
- 放大死锁/卡顿风险;
- 让 privileged operation 阻塞页面。
Electron 官方建议尽量避免它。
如果一个操作本质异步,优先 invoke/handle。
旧式 send + reply
历史上可以:
send(request)
→ ipcMain.on
→ event.reply(response)
构造双向通信。
但它需要自己处理:
- request correlation;
- listener lifecycle;
- error mapping;
- concurrent requests。
对普通 request/response,invoke/handle 更简洁。
Preload 不应该暴露原始 IPC
错误方向:
window.ipc.send(channel, payload)
window.ipc.invoke(channel, payload)
页面可以自己选择 channel。
更安全:
window.files.open()
window.settings.load()
window.updater.install()
每个方法映射到固定 IPC capability。
Sender Validation
IPC 来自 renderer 时,main 不能因为“这是我自己的窗口”就默认信任。
Electron 官方安全清单明确建议:
validate the sender of all IPC messages.
尤其当存在:
- iframe;
- remote content;
- child window;
- navigation;
- multiple WebContents;
时,要验证 event.senderFrame / origin 等来源信息。
参数 Validation
即使 channel 固定:
deleteFile(path)
仍然必须验证:
- path 类型;
- path 是否在允许目录;
- symlink / traversal;
- 当前用户/业务权限;
- 当前 app state。
preload 的 TypeScript 类型不是安全验证。
Runtime input 仍应视为不可信。
Error Contract
ipcMain.handle 内抛出的 Error 跨 IPC 后不会透明保留完整对象。
Electron 当前文档明确指出,main handler 的 error 会序列化,renderer 主要得到 message 等有限信息。
因此复杂系统应自己定义稳定 error contract,例如:
{ ok: false, code: “FILE_NOT_ALLOWED”, message: ”…” }
而不是依赖 Node Error prototype 跨进程传播。
Event Subscription
对于 main → renderer event:
onUpdateAvailable(callback)
preload 应:
- 注册固定 channel;
- 丢弃底层 IPC event;
- 只把 payload 传给 callback;
- 返回 unsubscribe function。
这样 renderer 能在组件 unmount 时清理监听器。
Channel 命名
channel 名称本身没有语义魔法。
类似:
dialog:openFile
settings:get
updater:install
只是 namespace convention。
真正安全性来自:
- fixed capability;
- sender validation;
- input validation;
- authorization;
- narrow return value。
大数据与高频通信
IPC 有 serialization / copy / scheduling 成本。
不要把:
- 高频鼠标事件;
- 大二进制对象;
- 每帧状态;
- 超大 JSON;
机械地通过普通 invoke/send 往返。
需要时考虑:
- batch;
- MessagePort / postMessage;
- Transferable;
- shared/streaming design;
- 将计算放在正确进程。
Renderer 不是 Trusted Backend
一个重要心智模型:
Renderer compromise 应被当作现实威胁模型。
所以 main IPC handler 类似本地 privileged API endpoint。
应像服务端 API 一样:
- authenticate source;
- validate input;
- authorize action;
- minimize output;
- log sensitive operation。
模式选择表
| 需求 | 首选模式 |
|---|---|
| renderer 通知 main,无结果 | send / on |
| renderer 调 main,需要结果 | invoke / handle |
| main 主动通知 renderer | webContents.send / renderer listener |
| 同步拿结果 | 尽量避免 sendSync |
| 任意 channel RPC | 不推荐 |
| 高权限操作 | fixed API + sender/input validation |
常见失败模式
万能 RPC
invoke(channel, args)
→ renderer 获得过宽能力。
没验证 sender
→ 意外 frame / remote page 可能调用 privileged handler。
只做 TypeScript 校验
→ runtime attacker 不受 TS 类型限制。
main 抛原始 Error 给 renderer
→ contract 不稳定,内部细节可能泄漏。
on() 监听器永不清理
→ 重复回调与内存泄漏。
sendSync 做文件或网络 I/O
→ renderer 卡顿。
学习检查
- send/on、invoke/handle、webContents.send 分别解决哪类通信?
- 为什么 request/response 通常优先 invoke/handle?
- 为什么万能 invoke(channel) 会削弱 preload capability boundary?
- sender validation 与 payload validation 为什么必须同时存在?
- 为什么 TypeScript 类型不能替代 runtime validation?
- main handler 的 Error 为什么不适合作为跨 IPC 稳定 contract?
- main→renderer event 的 preload wrapper 为什么应该隐藏原始 IPC event 并返回 unsubscribe?
- 给“打开文件、保存设置、更新可用通知”三个需求分别选择 IPC 模式。