Electron MessageChannelMain 详解在主进程中创建消息通道对与双向通信【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electronMessageChannelMain是 Electron 主进程侧对应 DOMMessageChannel对象的 API其唯一职责是创建一对相互连通的MessagePortMain。借助它主进程可以主动发起一条独立的消息通道把其中一个端点经WebContents.postMessage或ipcRenderer.postMessage投递给渲染进程之后主进程与渲染进程即可在这条私有通道上双向收发数据甚至让两个渲染进程绕过主进程直接互连。读完本文你将掌握MessageChannelMain的完整用法、postMessage/start/close的语义、基于EventEmitter的事件模型差异以及该 API 在 C 层的createPair实现与参数校验规则。类概览主进程中的 Channel MessagingElectron 的渲染进程天然拥有 Web 标准的MessageChannel/MessagePort但主进程没有 Blink 环境因此不存在这两个类。为了让主进程也能参与通道消息Channel MessagingElectron 提供了两个主进程专用类MessageChannelMain主进程侧的MessageChannel等价物从electron模块直接导出可通过new MessageChannelMain()实例化MessagePortMain主进程侧的MessagePort等价物不从electron模块导出只能作为MessageChannelMain的port1/port2属性或 IPC 消息的event.ports返回值获得。MessagePortMain与 DOM 版本的关键差异在于事件系统它基于 Node.js 的EventEmitter而不是 Web 的EventTarget。因此监听消息应使用port.on(message, ...)而不是port.onmessage ...或port.addEventListener(message, ...)。官方文档message-channel-main.md给出的最小示例如下主进程创建通道、发送一个端点并推送消息渲染进程通过ipcRenderer.on(port, ...)接收// Main process const { BrowserWindow, MessageChannelMain } require(electron) const w new BrowserWindow() const { port1, port2 } new MessageChannelMain() w.webContents.postMessage(port, null, [port2]) port1.postMessage({ some: message }) // Renderer process const { ipcRenderer } require(electron) ipcRenderer.on(port, (e) { // e.ports is a list of ports sent along with this message e.ports[0].onmessage (messageEvent) { console.log(messageEvent.data) } })需要注意两点端点只能经postMessage系列方法传递——常规的send/invoke等 IPC 方法无法传输MessagePort只有WebContents.postMessage和ipcRenderer.postMessage支持在transfer参数中携带端点在另一端注册监听器之前发送的消息不会丢失会被缓存直到对端就绪后按序投递。另外与 Electron 所有内置类一样MessageChannelMain无法在用户代码中被子类化见 FAQ 中 Class inheritance does not work with Electron built-in modules 一节。实例属性port1与port2new MessageChannelMain()返回的通道对象只有两个实例属性channel.port1一个MessagePortMainchannel.port2一个MessagePortMain。两个端点本身没有区别唯一差别在于用法发送到port1的消息由port2收到反之亦然。它们构成一个通道任何一端都可以独立地调用postMessage、start、close。端点的核心方法来自 MessagePortMain由于端点是MessagePortMain其完整能力定义在 message-port-main.md 中这里一并说明以便完整使用通道port.postMessage(message, [transfer])从该端口发送消息message为任意可结构化克隆的值transfer可选为MessagePortMain[]表示把其他端点的所有权转移给消息接收方port.start()开始处理端口上排队的消息。在调用start()之前收到的消息会被缓存port.close()断开端口使其不再活跃并触发本端的close事件事件message收到消息时触发参数messageEvent含data消息体any与ports随消息转移来的MessagePortMain[]事件close对端断开时触发。这是 Electron 相对 Web 标准的扩展——Web 上没有此语义在 Web 上对应port.onclose/addEventListener(close)Electron 额外保证了主进程侧的等价能力并且端口被垃圾回收时也会隐式触发关闭。源码剖析createPair是如何连通的从 JS 层看MessageChannelMain的实现非常薄——lib/browser/api/message-channel.tsimport { MessagePortMain } from electron/internal/browser/message-port-main; const { createPair } process._linkedBinding(electron_browser_message_port); export default class MessageChannelMain implements Electron.MessageChannelMain { port1: MessagePortMain; port2: MessagePortMain; constructor() { const { port1, port2 } createPair(); this.port1 new MessagePortMain(port1); this.port2 new MessagePortMain(port2); } }构造函数通过process._linkedBinding(electron_browser_message_port).createPair()在 C 层创建一对原生端点再用MessagePortMain包装成 JS 对象。该类经由 lib/browser/api/module-list.ts 注册进浏览器进程的模块列表{ name: MessageChannelMain, loader: () require(./message-channel) }所以它才能直接从require(electron)中拿到。JS 侧的包装类 lib/browser/message-port-main.ts 做了两件事把原生端点的事件桥接成EventEmitter事件收到message时随消息转移进来的子端口也会被逐个重新包装为MessagePortMain再抛出以及在postMessage时把参数数组中包装过的MessagePortMain还原为内部原生端点后再传给_internalPort.postMessage。真正的成对连通发生在 C 层的 shell/browser/api/message_port.ccv8::Localv8::Value CreatePair(v8::Isolate* isolate) { auto* port1 MessagePort::Create(isolate); auto* port2 MessagePort::Create(isolate); blink::MessagePortDescriptorPair pipe; port1-Entangle(pipe.TakePort0()); port2-Entangle(pipe.TakePort1()); // ... 包装成 { port1, port2 } 对象返回 }可以看到MessagePortshell/browser/api/message_port.h被注释为A non-blink version of blink::MessagePort——它是blink::MessagePort的主进程移植版。CreatePair使用blink::MessagePortDescriptorPair一对底层句柄生成 pipe并让两个端点分别Entangle两端句柄从而在 Mojo 管道层面完成互连。几个值得注意的实现细节端口默认处于暂停状态Entangle中调用connector_-PauseIncomingMethodCallProcessing()message_port.cc只有start()才调用ResumeIncomingMethodCallProcessing()message_port.cc。这正是文档中消息在start()之前会排队这一行为的底层机制close事件来自连接错误处理Entangle时注册了set_connection_error_handler对端管道断开或本端调用Close()时会发出close事件message_port.cc存活保护已start且已连通的端口具有待处理活动HasPendingActivity会通过SelfKeepAlive被 pin 住即使 JS 包装对象暂时不可达端点也不会被提前释放消息序列化PostMessage使用blink::TransferableMessage Mojo 传输message_port.cc因此消息体必须是可结构化克隆的值postMessage()传undefined、null等简单值是合法的。测试用例印证的行为边界MessageChannelMain的行为在 spec/api-ipc-spec.ts 中有专门的测试套件从中可以提炼出几条实用的行为边界postMessage接受的结构化克隆值undefined、数字42、false、数组、字符串、对象{ hello: goodbye }都能正常发送并送达对端transfer 参数必须严格为端点数组port1.postMessage(null, {})、postMessage(null, [buffer])、postMessage(null, [1])、postMessage(null, [new Date()])都会抛错C 层DisentanglePorts会对 transfer 数组逐个校验——非端口值抛 is not a valid port重复端口抛 is a duplicate已断开端点抛 is already neuteredmessage_port.cc不能把源端口自身放入 transferport1.postMessage(null, [port1])会抛错HTML 规范 8.3.3 节的规则在 message_port.cc 中显式检查 contains the source port跨窗口、跨 SharedWorker 的端口转移测试中还验证了主进程创建MessageChannelMain后把一个端点postMessage给窗口、另一个端点转给SharedWorker消息可双向流动消息在无监听者时不丢失向尚未注册监听器的端点发送消息端点仍会保持存活由SelfKeepAlive保证监听器注册后消息按序投递。这些测试与上文源码分析相互印证MessageChannelMain本质上是一根由 Mojo 管道互连、带排队与所有权转移规则的双向消息管。典型实战场景以下示例取自 docs/tutorial/message-ports.md展示了MessageChannelMain最实用的三种用法。场景一让两个渲染进程直接互连主进程创建通道把两端分别发给两个窗口两个渲染进程即可互相通信无需每条消息都经主进程转发const { BrowserWindow, app, MessageChannelMain } require(electron) app.whenReady().then(async () { const mainWindow new BrowserWindow({ show: false, webPreferences: { contextIsolation: false, preload: preloadMain.js } }) const secondaryWindow new BrowserWindow({ show: false, webPreferences: { contextIsolation: false, preload: preloadSecondary.js } }) // set up the channel. const { port1, port2 } new MessageChannelMain() // once the webContents are ready, send a port to each webContents with postMessage. mainWindow.once(ready-to-show, () { mainWindow.webContents.postMessage(port, null, [port1]) }) secondaryWindow.once(ready-to-show, () { secondaryWindow.webContents.postMessage(port, null, [port2]) }) })preload 中接收端口并挂接监听注意生产环境建议开启contextIsolation并用contextBridge封装示例为简写const { ipcRenderer } require(electron) ipcRenderer.on(port, (e) { window.electronMessagePort e.ports[0] window.electronMessagePort.onmessage (messageEvent) { // handle message } })场景二隐藏窗口作为工作进程端口直连避免主进程中转把隐藏的BrowserWindow当作拥有完整 Blink 上下文的 worker主进程只做一次性的通道握手之后应用窗口与 worker 直接对话const { BrowserWindow, app, MessageChannelMain } require(electron) app.whenReady().then(async () { const worker new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) await worker.loadFile(worker.html) const mainWindow new BrowserWindow({ webPreferences: { nodeIntegration: true } }) mainWindow.loadFile(app.html) // 不能用 ipcMain.handle()因为回复需要转移 MessagePort。 mainWindow.webContents.mainFrame.ipc.on(request-worker-channel, (event) { const { port1, port2 } new MessageChannelMain() worker.webContents.postMessage(new-client, null, [port1]) event.senderFrame.postMessage(provide-worker-channel, null, [port2]) // 现在主窗口与 worker 可以绕开主进程直接通信 }) })渲染端只需在收到provide-worker-channel后取出event.ports[0]注册onmessage并postMessage投递任务即可完整代码见 message-ports.md 的 Worker process 一节。场景三实现请求—流式响应内置 IPC 只有send发射即忘与invoke单次请求—响应两种模式而通道可以表达一次请求对应多条响应的流式语义渲染进程创建通道把一端交给主进程主进程持续postMessage多条数据后close()渲染进程通过onmessage接收、通过onclose感知流结束。主进程侧核心代码ipcMain.on(give-me-a-stream, (event, msg) { const [replyPort] event.ports for (let i 0; i msg.count; i) { replyPort.postMessage(msg.element) } replyPort.close() })场景四穿透 contextIsolation 直达页面主世界启用 context isolation 时主进程 IPC 默认落在隔离世界。若要把消息直接送入主世界可以让 preload 收到port后用window.postMessage(..., *, event.ports)把端口再转移到主世界——端口随postMessage跨世界传递后页面主世界即可与主进程直接通信。该方案完整代码见 message-ports.md 的 Communicating directly between the main process and the main world of a context-isolated page 一节。使用限制与注意事项结合文档与源码归纳MessageChannelMain的实际使用约束传递通道必须走postMessage家族WebContents.postMessage(channel, message, [transfer])与ipcRenderer.postMessage(channel, message, [transfer])是仅有的两个支持转移MessagePort的入口send/invoke/handle均不行所以凡是响应里要带端口的场景不能直接用ipcMain.handle需改用postMessage回复transfer 数组只能放端口且不能包含源端口自身、不能重复否则抛 TypeErrormessage_port.cc主进程端点需start()才收消息MessagePortMain遵循 Web 端点的排队语义未start()前消息被缓存底层PauseIncomingMethodCallProcessing实现端口会被垃圾回收隐式关闭两端都会因此收到close事件需要长期存活的通道应自行持有端口引用已start的端口由SelfKeepAlive机制自动保活message_port.cc;不能子类化与所有 Electron 内置类一致new (class extends MessageChannelMain {})()不可用。参考文档docs/api/message-channel-main.mdMessageChannelMain官方 API 文档docs/api/message-port-main.mdMessagePortMain的方法与事件定义docs/tutorial/message-ports.mdElectron 消息端点教程与完整示例集核心实现lib/browser/api/message-channel.ts、lib/browser/message-port-main.ts、shell/browser/api/message_port.cc、shell/browser/api/message_port.h行为测试spec/api-ipc-spec.ts 中的MessageChannelMain测试套件。【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考