Electron 中引入MessageChannel 大大缩短不同渲染进程和 Webview 各组件 1o1的通信链路
背景
在 electron 开发中,也不可避免地遇到端到端的通信问题,Electron 已经内置一些通信 API,但是实际用下来会发现,在引入 Webview 之后,通信链路会很长,参考 利用本地 Express Web 服务解决复杂的 Electron 通信链路的问题_ipcrenderer.invoke('save-data-CSDN博客的问题, 在这个过程中有一种机制可以有效解决通信链路的问题,这个就是 MessageChannel,MessageChannel 可以提供两个 port,一个自己拿着,另外一个发给别人,以此来实现双向通信,有了这个东西,无论再远的距离,只需要有个中间人帮你把话筒拿给那个人,你们就可以实现互聊。
MessageChannel 在多 iframe 开发上的表现
纯用Grok 实现一个多 iframe 互聊对话框 利用父子框架通信和MessageChannel 实现 n 个 iframe 的动态创建——通用编码习惯-CSDN博客
代码仓库
electron-demo: electron 22 初始代码开发和讲解
https://e.gitee.com/sen2020/projects/203933/repos/sen2020/electron-demo/tree/feature%2Fmessageport
feature/messageport 分支
Electron 中的概念纠正
-
Webview 和 Renderer 是同级的,两者只有一个关联关系,也即 Webview 在那个 Renderer 中展现的,Webview 所有运行方式以及通信方式,均与 Renderer 没有任何差别,Webview 可以直接与主进程通信,这个概念特别重要,否则就会绕一大圈,Electron 官网给了一个 sendToHost API,误解性很大,让我们误以为只有 sendToHost 才能与 Webview 依附的渲染通信,然后再中转给其他渲染进程或者 webview 等,实际上完全不需要
-
同级这个概念其实在代理拦截,打开外部链接,以及请求头替换等方面都有表现,只是被忽略了
-
preload.js 代码是被当作一个闭包执行的,如果你在 console 控制台查看时,会发现的确是一个闭包在执行,所以里面的函数和变量你不暴露在 window 上的话,注入 js 是拿不到这些函数,当然前提条件是,去掉上下文隔离。
去掉上下文隔离设置
必备知识
-
因为 Electron 主进程是基于 Node.js 环境来运行的,所以并不是 V8 的环境,要想实现类似 Web 环境的 MessageChannel,Electron 就必须自己重新实现,因此 Electron 中的 MessageChannel 类型为 MessageChannelMain,这里为了照顾系统起来后,就会立即推送信息过来,而主进程没有充分启动,导致消息丢失问题,Electron 团队追加了一个 port.start() 函数,也即主进程完全 OK 后,可开启消息消费。
-
主进程的MessageChannel 不重要,因为主进程并不创建MessageChannel,只管理来自其他 Renderer 和 Webview 的 channel.port,并将信息进行及时中转,因为所有的其他进程都可以直接联系到主进程,利用 send,invoke,sendSync,postMessage 等 API 都可以与主进程直接通信
-
Electron 官方很鼓励使用 MessagePort 进行通信,原因是这个通信机制是点对点通信,并不是像监听一样采用广播机制发送消息,在一定意义上对内存的耗费就大大降低了,所以更换 MessagePort 方向是对的
具体实现思路
-
在 preload.js 或者任意注入脚本任意位置的组件内部,都可以创建一个 MessageChannel,给自己起一个名字,并把自己注册到主进程去,之后就可以加入到大家庭了
-
在渲染进程中的任意一个 Vue 组件里面创建一个 MessageChannel,给自己起个名字,然后注册给主进程,之后就可以加入到大家庭了
-
主进程监听来自各个渲染进程和 Webview 组件的 MessageChannel 注册,并实现每个 port 广播和 from to 的中转逻辑即可
实现快照
代码实现
主进程添加以下代码
// 使用 Map 存储 MessagePort,键为名称,值为 MessagePort 对象 const ports = new Map(); // 监听渲染进程和 webview 的注册请求 ipcMain.on('register', (event, name) => { const port = event.ports[0]; // 获取传递的 MessagePort if (ports.has(name)) { port.postMessage({ type: 'error', data: `Name "${name}" already registered`, }); return; } ports.set(name, port); // 监听该 MessagePort 的消息 port.on('message', (event) => { const { from, to, data } = event.data; console.log(`Forwarding message from ${from} to ${to}`); if (to === 'all') { // 广播给所有其他 MessagePort ports.forEach((p, pName) => { if (pName !== from) { p.postMessage({ from, data }); } }); } else if (ports.has(to)) { // 转发给指定的 MessagePort ports.get(to).postMessage({ from, data }); } else { // 目标不存在,发送错误消息 ports.get(from).postMessage({ type: 'error', data: `Target ${to} not found`, }); } }); // 监听关闭事件 port.on('close', () => { console.log(`${name} 的 MessagePort 已关闭`); ports.delete(name); }); // 开始接收消息 port.start(); // 向注册者发送注册成功消息 port.postMessage({ type: 'registered', name }); });
渲染进程添加如下代码,任意位置,代码是通用的,只需要改改 name 即可
// 创建 MessageChannel const { port1, port2 } = new MessageChannel(); const myPort = port2; const myName = "mainWindow" // 将 port1 发送给主进程,附带名称 ipcRenderer.postMessage('register', myName, [port1]); // 监听来自主进程的消息 myPort.onmessage = (event) => { const { type, from, data } = event.data; console.log(`Received message from ${from}`); if (type === 'registered') { console.log(`注册成功: ${myName}`); } else if (type === 'error') { console.log(`错误: ${data}`); } else { console.log(`[${from}]: ${data}`); } }; // 发送消息给另一个进程 function sendMessage(to, data) { myPort.postMessage({ from: myName, to, data }); } // 发送广播消息 function broadcastMessage(data) { myPort.postMessage({ from: myName, to: 'all', data }); } window.sendMessage = sendMessage; window.broadcastMessage = broadcastMessage; // 示例用法 setTimeout(() => { sendMessage('wb1', 'Hello from renderer1!'); broadcastMessage('Broadcast from renderer1!'); }, 10000); }
preload.js 中的实现代码,基本和渲染进程是一样的
// 因为WhatsApp也有个require函数,这里会干扰WhatsApp加载 delete window.require; // 这里是node.js环境,必须用require引入 const {ipcRenderer} = require("electron") ; // 创建 MessageChannel const { port1, port2 } = new MessageChannel(); window.myPort = port2; window.myName = "wb1" // 将 port1 发送给主进程,附带名称 ipcRenderer.postMessage('register', myName, [port1]); // 监听来自主进程的消息 myPort.onmessage = (event) => { const { type, from, data } = event.data; console.log(`Received message from ${from}`); if (type === 'registered') { console.log(`注册成功: ${myName}`); } else if (type === 'error') { console.log(`错误: ${data}`); } else { console.log(`[${from}]: ${data}`); } }; // 发送消息给另一个进程 function sendMessage(to, data) { myPort.postMessage({ from: myName, to, data }); } window.sendMessage = sendMessage; // 发送广播消息 function broadcastMessage(data) { myPort.postMessage({ from: myName, to: 'all', data }); } window.broadcastMessage = broadcastMessage; // 示例用法 setTimeout(() => { sendMessage('mainWindow', 'Hello from renderer1!'); broadcastMessage('Broadcast from renderer1!'); }, 2000);