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]; // 获取传递的 MessagePortif (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') {// 广播给所有其他 MessagePortports.forEach((p, pName) => {if (pName !== from) {p.postMessage({ from, data });}});} else if (ports.has(to)) {// 转发给指定的 MessagePortports.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 即可
// 创建 MessageChannelconst { 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);