进程通信
约 1770 字大约 6 分钟
2026-03-10
Electron 采用多进程架构,主进程主要负责管理应用的生命周期、窗口等,而渲染进程则负责渲染界面和处理用户交互。由于主进程和渲染进程运行在不同的进程中,因此它们不能像普通函数调用一样直接互相访问数据。
例如渲染进程中点击一个按钮,希望读取本地文件:
renderer 点击按钮
↓
需要通知 main
↓
main 读取文件
↓
main 把结果返回给 renderer这就是进程通信,也就是 IPC
在 Electron 中,开发者常见的 IPC API 是:
// 用于渲染进程和主进程之间的通信
ipcRenderer.invoke()
// 用于主进程注册处理函数,响应渲染进程的 invoke 请求
ipcMain.handle()
// 用于渲染进程向主进程发送消息
ipcRenderer.send()
// 用于主进程监听渲染进程发送的消息
ipcMain.on()
// 用于主进程向渲染进程发送消息
webContents.send()
// 用于渲染进程监听主进程发送的消息
ipcRenderer.on()这些 API 在 JavaScript 层看起来很简单,但底层仍然要依赖 Chromium 的进程通信能力。
Chromium 的 Mojo 框架
Electron 内部集成了 Chromium,因此它的进程通信能力建立在 Chromium 的 IPC 体系之上。在 Chromium 中,底层 IPC 主要由 Mojo 框架提供。
Electron 并不是自己从零实现一套跨进程通信系统,而是在 Chromium 的 Mojo 能力之上,封装出了面向 Electron 开发者的 ipcMain 和 ipcRenderer API。
Electron IPC API
↓
Electron C++ / JS 封装
↓
Chromium Mojo
↓
操作系统进程通信能力Electron 的 IPC 分层
Electron 的 IPC 可以分成三层理解:
第一层:开发者 API
ipcRenderer.invokeipcMain.handleipcRenderer.sendipcMain.on
第二层:Electron 内部封装
lib/browser/api/*lib/renderer/api/*shell/browser/api/*shell/renderer/api/*
第三层:Chromium Mojo
负责真正跨进程传递消息
在 Electron 源码中,shell/common/api/api.mojom 定义了主进程和渲染进程之间通信需要用到的 Mojo 接口。其中比较关键的是:
| 接口名称 | 所在进程 | 作用 |
|---|---|---|
ElectronBrowser | 主进程 | 定义主进程可以接收的 IPC 调用 |
ElectronRenderer | 渲染进程 | 定义渲染进程可以接收的 IPC 调用 |
在编译阶段会把 api.mojom 添加到编译配置文件中,接下来在编译 Electron 源码的时候,Mojo 框架会把这两个通信接口:转译为具体的实现代码并将其写入到 shell/common/api/api.mojom.h 头文件中。
下面两个文件都会引用上面的头文件:
shell/renderer/api/electron_api_ipc_renderer.ccshell/browser/api/electron_api_web_contents.cc
invoke / handle 通信流程
ipcRenderer.invoke 和 ipcMain.handle 是一组请求-响应式通信 API。开发者侧代码通常是:
// renderer
const result = await ipcRenderer.invoke('read-file', filePath)// main
ipcMain.handle('read-file', async (event, filePath) => {
return fs.readFileSync(filePath, 'utf-8')
})渲染进程通过 invoke 发起一次请求,主进程通过 handle 注册对应的处理函数。主进程处理完成后,会把结果返回给渲染进程,因此渲染进程侧拿到的是一个 Promise。所以它和 ipcRenderer.send 不同。send 是发一条消息出去,默认不关心返回结果;而 invoke 是发起一次调用,并等待主进程返回结果。
简化流程
ipcRenderer.invoke
-> renderer C++ 创建 Promise
-> Mojo 发送消息
-> browser C++ 接收消息
-> 触发 -ipc-invoke
-> JS 层查找 ipcMain.handle 注册的 handler
-> 执行 handler
-> 通过 callback 返回结果
-> renderer Promise resolve / reject渲染进程发起 invoke 请求
当渲染进程调用:
ipcRenderer.invoke('read-file', filePath)表面上看只是调用了一个 JavaScript 方法,但实际上会继续进入 Electron 在渲染进程侧暴露的 Native binding。在 Electron 源码中,渲染进程侧与 IPC 相关的实现位于下面的文件中:
shell/renderer/api/electron_api_ipc_renderer.cc当执行 Invoke 方法时,Electron 首先会创建一个 Promise。因为 invoke 是异步请求,主进程不会立刻返回结果,所以渲染进程必须先保存一个等待结果的 Promise。
v8::Local<v8::Promise::Resolver> resolver =
v8::Promise::Resolver::New(context).ToLocalChecked();这个 resolver 后续会在主进程返回结果时被触发。如果主进程正常返回,Promise 会被 resolve。如果主进程抛出异常,Promise 会被 reject。
因此开发者才可以写:
const result = await ipcRenderer.invoke('read-file', filePath)这里的
await等待的就是这个由底层创建出来的 Promise。
调用 Mojo 通信对象
创建 Promise 之后,渲染进程需要把这次调用发送给主进程。在渲染进程侧,Electron 会通过一个 Mojo remote 对象向主进程发送消息,例如:
electron_browser_remote_->Invoke(
channel,
std::move(args),
base::BindOnce(&OnInvokeResponse, std::move(resolver))
);这里可以拆成三部分理解:
channel表示本次调用的频道名,例如"read-file"。args表示渲染进程传给主进程的参数。OnInvokeResponse是主进程返回结果后要执行的回调,它会负责把结果写回前面创建的 Promise。
主进程接收 invoke 消息
消息通过 Mojo 到达主进程后,会进入 Electron browser 侧的 C++ 实现。对应逻辑位于:
shell/browser/api/electron_api_web_contents.cc主进程侧会有一个 WebContents::Invoke 的方法,用来接收来自渲染进程的 invoke 消息。示意代码如下:
void WebContents::Invoke(
const std::string& channel,
base::Value::List args,
InvokeCallback callback) {
EmitWithSender(
"-ipc-invoke",
web_contents(),
std::move(callback),
std::move(args)
);
}重要
这里主进程 C++ 层接收到 Mojo 消息后,并不会直接执行用户写的 ipcMain.handle 逻辑,而是先把它转换成 Electron 内部的事件 -ipc-invoke,然后再由 Electron 的 JS 层去处理这个事件并执行用户注册的 handler。
JS 层分发 invoke 事件
当主进程触发 -ipc-invoke 事件后,后续逻辑会进入 Electron 主进程侧的 JavaScript / TypeScript 层。这一层的任务是:找到开发者通过 ipcMain.handle(channel, handler) 注册的处理函数,并执行它。Electron 内部会维护一个 Map,用来保存 channel 和 handler 的关系。
当开发者写:
ipcMain.handle('read-file', async (event, filePath) => {
return fs.readFileSync(filePath, 'utf-8')
})本质上就是把这个处理函数注册到 Map 中,之后,当主进程收到 invoke 时,Electron 会根据 channel 去 Map 中查找对应的 handler。如果找到了,就执行这个 handler;如果没有找到,就会抛出类似下面的错误:No handler registered for 'read-file'
const invokeHandlers = new Map<string, Function>()
webContents.on('-ipc-invoke', async (event, channel, ...args) => {
const handler = invokeHandlers.get(channel)
if (!handler) {
throw new Error(`No handler registered for '${channel}'`)
}
const result = await handler(event, ...args)
})执行 handler 并返回结果
当 Electron 找到对应的 handler 后,会执行用户注册的函数。
ipcMain.handle('read-file', async (event, filePath) => {
return fs.readFileSync(filePath, 'utf-8')
})这里的返回值会被 Electron 捕获。如果 handler 正常返回:
return 'file content'那么这个结果会被传回渲染进程,渲染进程侧的 Promise 会被 resolve。
如果 handler 抛出异常:
throw new Error('read failed')那么错误信息会被传回渲染进程,渲染进程侧的 Promise 会被 reject。
