Electron 如何用 MessagePort 在主进程与渲染进程间传输消息与可转移对象?
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
在 Electron 应用里,当你需要把MessagePort交给对方进程、让两个进程之间走一条独立的通信信道(甚至实现“一次请求、多次回复”的回包流)时,会遇到一个硬性限制:常规的 IPC 方法send和invoke无法携带MessagePort,只有postMessage系列方法可以传输MessagePort。本文基于 MessagePorts in Electron 教程与 MessagePortMain、MessageChannelMain、ipcRenderer、WebContents 的 API 文档,给出从建信道、发端口到接收验证的完整操作路径。
先弄清哪些方法能传输 MessagePort
MessagePort是 Web 特性:它类似window.postMessage,但工作在独立信道上。MessagePort总是成对创建,一对相连的端口称为 channel(信道)。在渲染进程中,MessagePort类与 Web 上的行为完全一致。
主进程不是网页,没有 Blink 集成,因此没有MessagePort或MessageChannel类。Electron 为此新增了两个主进程类:
MessagePortMain:主进程侧的MessagePort等价物,使用 Node.js 的EventEmitter事件系统,所以监听消息要用port.on('message', ...)而不是port.onmessage = ...;MessageChannelMain:唯一作用是创建一对相连的MessagePortMain,通过channel.port1和channel.port2属性访问。
端口传递方向与到达形式:
| 发送方法 | 发送方 | 端口在接收方的形式 |
|---|---|---|
ipcRenderer.postMessage(channel, message, [transfer]) | 渲染进程 | 主进程中通过事件的event.ports属性获得MessagePortMain |
webContents.postMessage(channel, message, [transfer]) | 主进程 | 渲染进程中为原生 DOMMessagePort对象,同样通过event.ports访问 |
两个注意点:
- 常规的
send、invoke等方法不能用来传输MessagePort,只能用postMessage方法; MessagePortMain不从'electron'模块导出,它只能作为其他方法(如MessageChannelMain)的返回值获得。
路径一:渲染进程把端口交给主进程
渲染进程创建信道,用ipcRenderer.postMessage把其中一端端口发给主进程:
// renderer.js (Renderer Process) // MessagePorts are created in pairs. A connected pair of message ports is // called a channel. const channel = new MessageChannel() // The only difference between port1 and port2 is in how you use them. Messages // sent to port1 will be received by port2 and vice-versa. const port1 = channel.port1 const port2 = channel.port2 // It's OK to send a message on the channel before the other end has registered // a listener. Messages will be queued until a listener is registered. port2.postMessage({ answer: 42 }) // Here we send the other end of the channel, port1, to the main process. ipcRenderer.postMessage('port', null, [port1])主进程在 IPC 事件里取到端口。文档示例中,到达的event.data是{ answer: 42 }(文档示例值,下同):
// main.js (Main Process) // In the main process, we receive the port. ipcMain.on('port', (event) => { // When we receive a MessagePort in the main process, it becomes a // MessagePortMain. const port = event.ports[0] // MessagePortMain uses the Node.js-style events API, rather than the // web-style events API. So .on('message', ...) instead of .onmessage = ... port.on('message', (event) => { // data is { answer: 42 } const data = event.data }) // MessagePortMain queues messages until the .start() method has been called. port.start() })这里有两个容易踩坑的点:
- 主进程收到的是
MessagePortMain,事件风格是 Node.js 的EventEmitter(port.on('message', ...)),不是 Web 风格的onmessage赋值。 MessagePortMain在调用port.start()之前会把消息排队,不调start()消息不会派发。反过来,在另一端注册监听器之前就可以先发消息,消息会排队等待。
postMessage的transfer参数只用于转移端口:ipcRenderer.postMessage的transfer类型是MessagePort[],webContents.postMessage的是MessagePortMain[]。消息体本身走结构化克隆,可以是任意可序列化对象——文档的 worker 示例中明确提到,事件数据可以是任意可序列化对象,并且事件里还能携带其他MessagePort。
路径二:主进程把端口发给渲染进程
主进程用MessageChannelMain建信道,用webContents.postMessage把一端发过去:
// 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) } })端口到达渲染进程后就是原生 DOMMessagePort,可以直接用 Web 风格的onmessage接收。主进程保留的port1用MessagePortMain.postMessage(message, [transfer])发消息,该方法同样支持转移对象的所有权。
回包流:一次请求、多次回复
内置 IPC 只支持两种模式:即发即忘(send)和请求-响应(invoke)。用MessageChannel可以实现“回包流”:一次请求对应一串回复。渲染进程每次请求新建一个信道(信道很轻量,每次请求建一个开销很小),把一端发给主进程,自己留另一端接收回复和结束信号:
// renderer.js (Renderer Process) const makeStreamingRequest = (element, callback) => { // MessageChannels are lightweight--it's cheap to create a new one for each // request. const { port1, port2 } = new MessageChannel() // We send one end of the port to the main process ... ipcRenderer.postMessage( 'give-me-a-stream', { element, count: 10 }, [port2] ) // ... and we hang on to the other end. port1.onmessage = (event) => { callback(event.data) } port1.onclose = () => { console.log('stream ended') } } makeStreamingRequest(42, (data) => { console.log('got response data:', data) }) // We will see "got response data: 42" 10 times.主进程端从event.ports取到回复端口,把结果逐条postMessage出去,发完后close():
// main.js (Main Process) ipcMain.on('give-me-a-stream', (event, msg) => { // The renderer has sent us a MessagePort that it wants us to send our // response over. const [replyPort] = event.ports // Here we send the messages synchronously, but we could just as easily store // the port somewhere and send messages asynchronously. for (let i = 0; i < msg.count; i++) { replyPort.postMessage(msg.element) } // We close the port when we're done to indicate to the other end that we // won't be sending any more messages. replyPort.close() })按文档示例,运行结果应看到 10 次got response data: 42,流结束时打印stream ended。close()不是严格必需的——如果显式关闭,端口最终会被垃圾回收,同样会触发对端的close事件;显式关闭只是明确告知对端不再发送。
结果验证
验证方式就是观察各端控制台输出,以上均为文档示例给出的预期输出:
- 基础路径:主进程在
port.on('message', ...)回调里读到data为{ answer: 42 }; - 主→渲染路径:渲染进程
onmessage回调里console.log(messageEvent.data)打印主进程发来的对象; - 回包流:10 行
got response data: 42加一次stream ended; - 信道断开:
close事件在信道另一端关闭(或被垃圾回收而隐式关闭)时触发。这是 Electron 在MessagePort上添加的、Web 上不存在的事件:渲染进程里可以用port.onclose或port.addEventListener('close', ...)监听,主进程里用port.on('close', ...)。
限制与注意事项
- 只有
postMessage系列方法(ipcRenderer.postMessage、webContents.postMessage,以及event.senderFrame.postMessage)能传输MessagePort;send、invoke不行。worker 示例里主进程之所以用mainFrame.ipc.on监听、用senderFrame.postMessage回复,就是因为ipcMain.handle的回复路径无法转移MessagePort。 - 主进程侧的
MessagePortMain必须调用start()后才派发排队的消息;渲染进程侧的 DOMMessagePort则是注册监听后直接接收。 MessagePortMain不从'electron'模块导出,只能从MessageChannelMain等 API 获得。- Electron 的内置类不能在你的代码中被继承(见 FAQ)。
- 端口可以被垃圾回收而隐式关闭,两端都会收到
close事件。
教程中还给出了几个与本文同一机制的进阶用例,可按需阅读:主进程作为中继让两个渲染进程互发消息、用隐藏窗口充当 worker 进程直接通信、以及开启 context isolation 时把端口送入页面主世界(MessagePorts in Electron)。
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考