首页
/ Electron MessageChannelMain 详解:在主进程中创建消息通道对与双向通信

Electron MessageChannelMain 详解:在主进程中创建消息通道对与双向通信

2026-09-06 09:01:21作者:曹令琨Iris

MessageChannelMain 是 Electron 主进程侧对应 DOM MessageChannel 对象的 API,其唯一职责是创建一对相互连通的 MessagePortMain。借助它,主进程可以主动发起一条独立的"消息通道":把其中一个端点经 WebContents.postMessageipcRenderer.postMessage 投递给渲染进程,之后主进程与渲染进程即可在这条私有通道上双向收发数据,甚至让两个渲染进程绕过主进程直接互连。读完本文,你将掌握 MessageChannelMain 的完整用法、postMessage/start/close 的语义、基于 EventEmitter 的事件模型差异,以及该 API 在 C++ 层的 createPair 实现与参数校验规则。

类概览:主进程中的 Channel Messaging

Electron 的渲染进程天然拥有 Web 标准的 MessageChannel/MessagePort,但主进程没有 Blink 环境,因此不存在这两个类。为了让主进程也能参与通道消息(Channel Messaging),Electron 提供了两个主进程专用类:

  • MessageChannelMain:主进程侧的 MessageChannel 等价物,从 'electron' 模块直接导出,可通过 new MessageChannelMain() 实例化;
  • MessagePortMain:主进程侧的 MessagePort 等价物,不从 'electron' 模块导出,只能作为 MessageChannelMainport1/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)
  }
})

需要注意两点:

  1. 端点只能经 postMessage 系列方法传递——常规的 send/invoke 等 IPC 方法无法传输 MessagePort,只有 WebContents.postMessageipcRenderer.postMessage 支持在 transfer 参数中携带端点;
  2. 在另一端注册监听器之前发送的消息不会丢失,会被缓存,直到对端就绪后按序投递。

另外,与 Electron 所有内置类一样,MessageChannelMain 无法在用户代码中被子类化(见 FAQ 中 "Class inheritance does not work with Electron built-in modules" 一节)。

实例属性:port1port2

new MessageChannelMain() 返回的通道对象只有两个实例属性:

  • channel.port1:一个 MessagePortMain
  • channel.port2:一个 MessagePortMain

两个端点本身没有区别,唯一差别在于用法:发送到 port1 的消息由 port2 收到,反之亦然。它们构成一个"通道",任何一端都可以独立地调用 postMessagestartclose

端点的核心方法(来自 MessagePortMain)

由于端点是 MessagePortMain,其完整能力定义在 message-port-main.md 中,这里一并说明以便完整使用通道:

  • port.postMessage(message, [transfer]):从该端口发送消息,message 为任意可结构化克隆的值;transfer(可选)为 MessagePortMain[],表示把其他端点的所有权转移给消息接收方;
  • port.start():开始处理端口上排队的消息。在调用 start() 之前,收到的消息会被缓存
  • port.close():断开端口,使其不再活跃,并触发本端的 'close' 事件;
  • 事件 'message':收到消息时触发,参数 messageEventdata(消息体,any)与 ports(随消息转移来的 MessagePortMain[]);
  • 事件 'close':对端断开时触发。这是 Electron 相对 Web 标准的扩展——Web 上没有此语义(在 Web 上对应 port.onclose / addEventListener('close'),Electron 额外保证了主进程侧的等价能力),并且端口被垃圾回收时也会隐式触发关闭。

源码剖析:createPair 是如何连通的

从 JS 层看,MessageChannelMain 的实现非常薄——lib/browser/api/message-channel.ts

import { 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.cc

v8::Local<v8::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()undefinednull 等简单值是合法的。

测试用例印证的行为边界

MessageChannelMain 的行为在 spec/api-ipc-spec.ts 中有专门的测试套件,从中可以提炼出几条实用的行为边界:

  1. postMessage 接受的结构化克隆值undefined、数字(42)、false、数组、字符串、对象({ hello: 'goodbye' })都能正常发送并送达对端;
  2. 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 neutered"(message_port.cc);
  3. 不能把源端口自身放入 transferport1.postMessage(null, [port1]) 会抛错(HTML 规范 8.3.3 节的规则,在 message_port.cc 中显式检查 "contains the source port");
  4. 跨窗口、跨 SharedWorker 的端口转移:测试中还验证了主进程创建 MessageChannelMain 后把一个端点 postMessage 给窗口、另一个端点转给 SharedWorker,消息可双向流动;
  5. 消息在无监听者时不丢失:向尚未注册监听器的端点发送消息,端点仍会保持存活(由 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] 注册 onmessagepostMessage 投递任务即可(完整代码见 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 数组只能放端口:且不能包含源端口自身、不能重复,否则抛 TypeError(message_port.cc);
  • 主进程端点需 start() 才收消息MessagePortMain 遵循 Web 端点的排队语义,未 start() 前消息被缓存(底层 PauseIncomingMethodCallProcessing 实现);
  • 端口会被垃圾回收隐式关闭:两端都会因此收到 'close' 事件,需要长期存活的通道应自行持有端口引用(已 start 的端口由 SelfKeepAlive 机制自动保活,message_port.cc);
  • 不能子类化:与所有 Electron 内置类一致,new (class extends MessageChannelMain {})() 不可用。

参考文档

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388