Electron 中引入MessageChannel 大大缩短不同渲染进程和 Webview 各组件 1o1的通信链路

发布于:2025-04-18 ⋅ 阅读:(19) ⋅ 点赞:(0)

背景

在 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 中的概念纠正

  1. Webview 和 Renderer 是同级的,两者只有一个关联关系,也即 Webview 在那个 Renderer 中展现的,Webview 所有运行方式以及通信方式,均与 Renderer 没有任何差别,Webview 可以直接与主进程通信,这个概念特别重要,否则就会绕一大圈,Electron 官网给了一个 sendToHost API,误解性很大,让我们误以为只有 sendToHost 才能与 Webview 依附的渲染通信,然后再中转给其他渲染进程或者 webview 等,实际上完全不需要

  2. 同级这个概念其实在代理拦截,打开外部链接,以及请求头替换等方面都有表现,只是被忽略了

  3. preload.js 代码是被当作一个闭包执行的,如果你在 console 控制台查看时,会发现的确是一个闭包在执行,所以里面的函数和变量你不暴露在 window 上的话,注入 js 是拿不到这些函数,当然前提条件是,去掉上下文隔离。

去掉上下文隔离设置

必备知识

  1. 因为 Electron 主进程是基于 Node.js 环境来运行的,所以并不是 V8 的环境,要想实现类似 Web 环境的 MessageChannel,Electron 就必须自己重新实现,因此 Electron 中的 MessageChannel 类型为 MessageChannelMain,这里为了照顾系统起来后,就会立即推送信息过来,而主进程没有充分启动,导致消息丢失问题,Electron 团队追加了一个 port.start() 函数,也即主进程完全 OK 后,可开启消息消费。

  2. 主进程的MessageChannel 不重要,因为主进程并不创建MessageChannel,只管理来自其他 Renderer 和 Webview 的 channel.port,并将信息进行及时中转,因为所有的其他进程都可以直接联系到主进程,利用 send,invoke,sendSync,postMessage 等 API 都可以与主进程直接通信

  3. Electron 官方很鼓励使用 MessagePort 进行通信,原因是这个通信机制是点对点通信,并不是像监听一样采用广播机制发送消息,在一定意义上对内存的耗费就大大降低了,所以更换 MessagePort 方向是对的

具体实现思路

  1. 在 preload.js 或者任意注入脚本任意位置的组件内部,都可以创建一个 MessageChannel,给自己起一个名字,并把自己注册到主进程去,之后就可以加入到大家庭了

  2. 在渲染进程中的任意一个 Vue 组件里面创建一个 MessageChannel,给自己起个名字,然后注册给主进程,之后就可以加入到大家庭了

  3. 主进程监听来自各个渲染进程和 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);


网站公告

今日签到

点亮在社区的每一天
去签到