Electron 进程通信总结
一、为什么需要 IPC
Electron 有两个进程:
| 进程 | 职责 | 能否直接访问 |
|---|---|---|
| 主进程 (main) | 创建窗口、系统 API、文件、菜单 | Node.js 全部能力 |
| 渲染进程 (renderer) | 网页 UI | 默认只有浏览器环境,无 Node |
两者内存隔离,不能直接调用对方函数,必须通过 IPC(进程间通信)。
二、四种基本模式
| 模式 | 主进程 | 渲染进程 | 是否带返回值 |
|---|---|---|---|
| 渲染 → 主(单向) | ipcMain.on(ch, cb) |
ipcRenderer.send(ch, ...args) |
❌ |
| 渲染 → 主(请求/响应) | ipcMain.handle(ch, cb) |
ipcRenderer.invoke(ch, ...args) |
✅ Promise |
| 主 → 渲染(单向) | webContents.send(ch, ...args) |
ipcRenderer.on(ch, cb) |
❌ |
| 主 → 渲染(请求/响应) | 无内置,需两个频道自己实现 | 无内置 | — |
记忆口诀
- 渲染要问主进程 →
invoke/handle(最常用,有返回) - 主进程要通知渲染 →
webContents.send/on - 渲染只上报不等待 →
send/on
unknown Record
三、方向速查图
┌─────────────────────────────────────────┐
│ 主进程 main │
│ ipcMain.on / ipcMain.handle │
│ webContents.send │
└───────▲──────────────────────┬──────────┘
│ │
invoke/send │ │ webContents.send
(渲染→主) │ │ (主→渲染)
│ ▼
┌───────┴──────────────────────┴──────────┐
│ 渲染进程 renderer │
│ ipcRenderer.invoke / send / on │
└─────────────────────────────────────────┘
四、两种方向的完整写法
1. 渲染 → 主(有返回)
// 主进程
ipcMain.handle('user:get', async (event, id) => {
return await db.findUser(id); // 返回值会作为 Promise resolve
});
// 渲染进程
const user = await ipcRenderer.invoke('user:get', 123);
2. 主 → 渲染(单向推送)
// 主进程
win.webContents.send('download:progress', { percent: 42 });
// 渲染进程
ipcRenderer.on('download:progress', (event, data) => {
console.log(data.percent);
});
3. 主 → 渲染,渲染要回复(自己拼)
// 主进程:发请求并等回复
let seq = 0;
const pending = new Map();
function ask(win, channel, payload) {
const id = ++seq;
return new Promise((resolve) => {
pending.set(id, resolve);
win.webContents.send(channel, { id, ...payload });
});
}
ipcMain.on('reply:channel', (e, { id, data }) => {
pending.get(id)?.(data);
pending.delete(id);
});
// 渲染进程
ipcRenderer.on('request:channel', (e, { id, ...payload }) => {
const result = doSomething(payload);
ipcRenderer.send('reply:channel', { id, data: result });
});
你代码里的
quick-cmd-form:action+quick-cmd-form:result就是这个模式。
五、安全:contextBridge + preload
不要在渲染进程直接暴露 ipcRenderer:
// ❌ 危险
contextBridge.exposeInMainWorld('ipc', ipcRenderer);
正确做法:preload 里只暴露白名单方法。
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', {
getUsers: () => ipcRenderer.invoke('user:list'),
onProgress: (cb) => {
const listener = (_e, data) => cb(data);
ipcRenderer.on('download:progress', listener);
return () => ipcRenderer.removeListener('download:progress', listener);
}
});
// 渲染进程
const users = await window.api.getUsers();
const off = window.api.onProgress((p) => console.log(p));
// 不用时 off();
要点:
- 只暴露具体方法,不暴露
ipcRenderer本体。 - 主进程侧用
authorized(event)校验来源窗口(你代码里已有)。 - 频道名、参数类型都要校验。
六、你项目里的 IPC 全景
以 quick-cmd-form 为例:
| 频道 | 方向 | 模式 |
|---|---|---|
quick-cmd-form:show |
主窗口 → 主进程 | invoke / handle |
quick-cmd-form:execute |
表单 → 主进程 | invoke / handle |
quick-cmd-form:hide |
表单 → 主进程 | invoke / handle |
quick-cmd-form:snippet-mode |
表单 → 主进程 | invoke / handle |
quick-cmd-form:result |
表单 → 主进程 | send / on |
quick-cmd-form:action |
主进程 → 表单 | webContents.send / on |
quick-cmd-form:progress |
主进程 → 表单 | webContents.send / on |
quick-cmd-form:reset |
主进程 → 表单 | webContents.send / on |
quick-cmd-form:key |
主进程 → 表单 | webContents.send / on |
quick-cmd-form:clear-console |
主进程 → 所有窗口 | webContents.send 广播 |
规律:
- 渲染主动要结果 →
invoke/handle(execute、hide、show) - 主进程主动通知 →
webContents.send(progress、reset、key、action) - 表单回传结果但不需要等待 →
send/on(result)
七、常见坑
| 坑 | 解决 |
|---|---|
窗口销毁后 send 报错 |
if (!win.isDestroyed()) win.webContents.send(...) |
页面没加载完就 send,消息丢失 |
等 did-finish-load,或渲染侧主动 invoke 拉取 |
on 重复注册,回调触发多次 |
保存 listener 引用,用 removeListener 清理 |
| 传了不可序列化对象(函数、DOM) | 只传普通对象/数组/字符串/数字/Buffer |
| 任意窗口都能调敏感 channel | authorized(event) 校验 event.sender |
直接暴露 ipcRenderer |
用 contextBridge 白名单封装 |
八、一句话总结
Electron IPC 就四件事:
- 渲染问主进程 →
invoke/handle(有返回,最常用) - 渲染通知主进程 →
send/on(不等待) - 主进程通知渲染 →
webContents.send/on(单向推送) - 主进程要渲染回复 → 两个频道 + id 配对,自己实现请求/响应
所有通信都要经过 preload + contextBridge 白名单 暴露,主进程侧用 authorized(event) 校验来源,才能保证安全。
misc
openDevTools
const inputWindow = new BrowserWindow({ width: 800, height: 600 });
inputWindow.loadFile('input.html');
inputWindow.webContents.openDevTools();
