Skip to main content

Electron IPC 是什么?

· 6 min read

Electron 的本质是两个不同类型的进程,IPC 是它们之间唯一合法沟通的桥。

  1. 进程模型:一个 main 进程(Node.js 全权限)+ N 个 renderer 进程(Chromium 受限沙箱)
  2. IPC 核心:main 用 ipcMain 监听,renderer 用 ipcRenderer 发起,contextBridge 是安全转交
  3. 三种调用模式send/on(单向)、invoke/handle(异步返回值)、MessagePort(双向长连接)
  4. 安全铁律:默认开 contextIsolation + nodeIntegration: false,主进程做权限守门人
  5. 反模式:在 renderer 里直接 require('fs')、把 token 放 IPC payload、不校验 sender

Electron 项目最容易出问题的地方不是 UI、不是打包,是 IPC。这一层如果搞错,要么是页面调不到 Node API,要么是把整个文件系统暴露给渲染进程,被 XSS 一键拖库。

这篇文章把 Electron IPC 一次讲透,从进程模型到安全配置。

一、Electron 为什么要拆成多个进程?

Electron 不是单个程序,至少由两类进程组成

进程数量运行时权限职责
main 进程1 个Node.js创建窗口、菜单、系统托盘、文件 IO、子进程
renderer 进程N 个(每窗口一个)Chromium受限渲染 HTML/JS/CSS、处理用户交互

renderer 进程默认不能直接访问 Node.js API。这是 Chromium 多进程架构的硬规则——每个 tab 跑在独立沙箱里,挂掉一个不影响其他。Electron 在此基础上让 renderer 也能加载本地资源(file://),但保留 Node 隔离。

那 renderer 想读文件、调用系统 API 怎么办?只能通过 IPC 找 main 进程代办

二、IPC 是什么?

IPC(Inter-Process Communication) 就是 main 和 renderer 之间的消息通道。

类比一下:

  • renderer 像浏览器里的前端页面
  • main 像后端 API 服务
  • IPC 就是它们之间的 HTTP

Electron 提供两种 API:

// main 进程(src/main.ts)
import { ipcMain } from 'electron';

ipcMain.handle('read-config', async () => {
return await fs.readFile('config.json', 'utf-8');
});

// renderer 进程(src/preload.ts 通过 contextBridge 暴露)
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('api', {
readConfig: () => ipcRenderer.invoke('read-config'),
});

// renderer 页面里直接用
const config = await window.api.readConfig();

三个角色各司其职:

  1. ipcMain:main 进程侧的消息接收器
  2. ipcRenderer:renderer 进程侧的消息发起器
  3. contextBridge:在 preload 脚本里把 ipcRenderer 方法白名单化给页面用

三、三种 IPC 调用模式

模式 1:send + on(单向,无返回值)

适合通知类消息("窗口已创建"、"开始下载"):

// renderer → main
ipcRenderer.send('log', '用户登录了');
// main
ipcMain.on('log', (event, msg) => console.log(msg));
send 是单向的,main 没办法直接回值给 renderer。

模式 2:invoke + handle(异步双向,最常用)

renderer 发起请求,main 返回 Promise:

// main
ipcMain.handle('save-file', async (_e, path, content) => {
await fs.writeFile(path, content);
return { ok: true };
});

// renderer
const result = await window.api.saveFile('/tmp/a.txt', 'hello');

这是 90% 场景的首选。

模式 3:MessagePort(建立长连接)

需要高频双向通信(实时数据流、长任务进度推送)时用:

// main 创建 port
const { port1, port2 } = new MessageChannelMain();
mainWindow.webContents.postMessage('init-port', null, [port2]);
// port1 留在 main 监听
port1.on('message', (e) => console.log(e.data));

四、安全配置:IPC 的 90% bug 出在这里

contextBridge 那一层不是装饰,是 安全护栏。默认配置下,renderer 是不能直接 require('fs') 的,但如果 preload 脚本写错,等于亲手把大门打开。

危险配置 vs 安全配置

// ❌ 危险:renderer 直接拿 Node API
new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});

// ✅ 安全:强制隔离
new BrowserWindow({
webPreferences: {
contextIsolation: true, // 默认就是 true,显式写出来
nodeIntegration: false, // 默认就是 false,显式写出来
sandbox: true, // 启用 OS 沙箱
preload: path.join(__dirname, 'preload.js'),
},
});

preload 是唯一的桥

// preload.ts - 只暴露白名单 API
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('api', {
readFile: (path: string) => ipcRenderer.invoke('fs:read', path),
});

// ❌ 反模式:把整个 ipcRenderer 交出去
contextBridge.exposeInMainWorld('ipc', ipcRenderer);
// 任何 XSS 都能 invoke 任意 channel,权限失控

main 端必须校验 sender

main 不能假设 invoke 进来的请求都合法:

ipcMain.handle('fs:read', async (event, path: string) => {
// 校验路径在允许目录内
const safe = path.startsWith('/allowed/');
if (!safe) throw new Error('Forbidden');

// 校验来源窗口是否受信任
if (!isTrustedWindow(event.senderFrame)) throw new Error('Untrusted');

return await fs.readFile(path, 'utf-8');
});
没有 sender 校验的 IPC handler 就是后端未鉴权的 API。

五、一个完整的例子:文件读写

main.ts:

import { app, BrowserWindow, ipcMain } from 'electron';
import { promises as fs } from 'fs';
import path from 'path';

ipcMain.handle('fs:read', async (_e, p: string) => {
return await fs.readFile(p, 'utf-8');
});

ipcMain.handle('fs:write', async (_e, p: string, content: string) => {
await fs.writeFile(p, content);
return { ok: true };
});

app.whenReady().then(() => {
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
win.loadFile('index.html');
});

preload.ts:

import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('fs', {
read: (p: string) => ipcRenderer.invoke('fs:read', p),
write: (p: string, content: string) => ipcRenderer.invoke('fs:write', p, content),
});

renderer 页面:

async function onRead() {
const text = await window.fs.read('/tmp/config.json');
console.log(text);
}

到这里一个最小可用闭环就完成了。

六、调试 IPC 必知

IPC 是异步 + 跨进程,错误信息经常 只在一端抛出,调试体验很差。三个必备技巧:

  1. handler 里 console.log(event.senderFrame)——能拿到来源 URL,定位是不是恶意 invoke
  2. webContents.on('ipc-message') 全局监听——记录所有 IPC 调用,便于排查调用链
  3. handler 第一个参数是 IpcMainInvokeEvent,不是普通 event,包含了 sendersenderFrame 等信息

七、常见踩坑

现象原因解决
window.api is undefinedpreload 没加载或路径错检查 webPreferences.preload 和打包后的 __dirname
invoke 返回 undefinedhandler 没 returnhandler 必须有返回值
频繁掉用卡顿每次 invoke 都序列化大对象用 MessagePort 走长连接
生产环境 IPC 失效contextIsolation 默认行为变了显式声明,别依赖默认
preload 报错但 renderer 不显示preload 错误会静默吞掉main 端 preload.js 改成 .ts 编译前先在 dev 单独跑一遍

八、总结

Electron IPC 三条铁律:

  1. main 是权限守门人:所有 Node API 必须在 main 端调用,renderer 只能通过 IPC 请求
  2. preload 是白名单出口contextBridge.exposeInMainWorld 暴露什么,页面就只能用什么,绝不交出整个 ipcRenderer
  3. handler 必须校验 sender:路径白名单 + 来源窗口校验,缺一不可

理解了进程模型,IPC 就是顺水推舟;理解不了,所有 Electron 项目最终都会演变成安全审计事故。

References

  1. Electron 官方文档 - Inter-Process Communication —— Electron 官方, 2026