大概是全网最详细的Electron ipc 讲解(三)——定情信物传声筒port

以下文章来源于LinDaiDai ,作者LinDaiDai

文章
《大概是全网最详细的Electron ipc 讲解(一)——主进程与渲染进程的两情相悦》,
《大概是全网最详细的Electron ipc 讲解(二)——渲染进程与渲染进程的搭桥牵线》

前言
本系列共有以下几个章节:

主进程与渲染进程的两情相悦
渲染进程与渲染进程的搭桥牵线
定情信物传声筒port
您此次阅读的是第三章节:定情信物传声筒port。

注:以上所有文章都被归档到:https://github.com/LinDaiDai/niubility-coding-js 中 ,案例都上传至:https://github.com/LinDaiDai/electron-ipc-example ,欢迎 Star,感谢 Star。

大纲
在之前的文章中,我们主要介绍了 ipcMain 和 ipcRenderer 是如何实现主进程与渲染进程、以及渲染进程与渲染进程进行通信的。大家不难发现,之前介绍的方式都非常依赖主进程,特别是渲染进程之间的通信,每次都需要主进程这个中间人来传话,难道就没有什么更简单点的方式吗?

咦,这还真有一个,那就是利用 MessagePort ,通俗易懂的翻译过来:消息端口...(啪,就你英语好是吧)

大家别急,还不知道是啥玩意的话,让我来给大家介绍一下。用个小故事来简单举个例子哈:

渲染进程一(某先生)和渲染进程二(某女士)通过媒人介绍认识,由于男俊女美且三观相符,很快两人就看对眼坠入爱河了,但是奈何工作地点不在一起,不得不异地。这刚认识还处于暧昧期的俩年轻人咋能忍住不联系呢。于是互相交换了手机号加上了wx,相亲结束后,每日通过手机互相联系,增进感情,不日,便确定关系,订婚,结婚,买房,生娃......停停停,给我回来。

咳咳咳,故事呢,其实到 "每日通过手机互相联系" 就结束了哈,后面自行脑补。在上面这则小故事中,媒人就是主进程,渲染进程一和二在最初,会通过主进程进行一个交换 port 的过程,后续都通过 port 来进行通信,不再依赖主进程了。

乍一看,是不是觉得这种通信方式比前面介绍的那些靠谱多了,而且还不需要通过主进程中继的性能开销。

讲完了故事,聊了个大概,让我们来看看本篇文章大纲吧:

MessagePort 的基础用法
主进程与渲染进程通信案例分析
渲染进程与渲染进程案例预告

1. MessagePort 的基础用法
1.1 如何创建 MessagePort
首先还是得先来看看 MessagePort 的基础用法。MessagePort 对象的创建依赖于 MessageChannel 类:

const channel = new MessageChannel();
const port1 = channel.port1
const port2 = channel.port2

// 或者简写为:
const { port1, port2 } = new MessageChannel();
实例化 MessageChannel 类之后,就产生了两个 port :port1 和 port2 。这两个 port 就是 MessagePort 对象。它就是我们上面那则故事提到的,可以用于两个进程之间进行长期通信的关键所在。

举个小例子,假设现在:

渲染进程一有了 port1
渲染进程二有了 port2
那么现在这两个进程就可以通过 port.onmessage 和 port.postMessage 来收发彼此间的消息了:

// 渲染进程一:
port1.onmessage = (event) => {
  console.log('received result:', event.data)
};
port1.postMessage('我是渲染进程一发送的消息');

// 渲染进程二:
port2.onmessage = (event) => {
  console.log('received result:', event.data)
};
port2.postMessage('我是渲染进程二发送的消息');
只要 port1 和 port2 一直都存在,它们就可以进行持久通信,怎么样,是不是很 niubility 。

OKK,那么现在如果是在渲染进程一创建的这两个 port ,关键就是如何把 port2 给到另一个渲染进程二了。也就涉及到了 MessagePort 的传递。

1.2 ipcRenderer.postMessage()
说到 MessagePort 的传递就得谈到 ipcRenderer 对象的 postMessage 方法了。因为 MessagePort 对象就是依靠它来传递。没错,此时应该有小伙伴可能想起来了,我们平常网页上的 window 对象也有一个 postMessage 方法,这两者之间其实挺像的,只不过呢,是在不同的通道上。

portMessage 它的参数如下:

ipcRenderer.postMessage(channle, message, [transfer])

channel String:事件名
message any:要传递的消息
transfer MessagePort[] (optional):0个或多个 MessagePort 对象。
前两个好理解,其实和 ipcRenderer 的其它方法差不多,事件,以及传递的消息。第三个参数有些特别,它是一个数组,其中可以传递 MessagePort 对象。这里需要注意,别看第三个参数标记的是 optional ,但其实它也是需要传递的,如果你不需要传 MessagePort 对象,那么就需要定义一个空数组,否则就会报错啦。

另外,在之前的文章中,我们还有用到 ipcRenderer 的其它方法: send、 invoke 、 sendSync ,这三种方法主进程都是可以给渲染进程传递返回结果的,比如:

//  render.js
const replyMessage = await ipcRenderer.invoke('render-invoke-to-main', '我是渲染进程通过 invoke 发送的消息');
console.log('replyMessage', replyMessage); // "我是主进程返回的消息"
postMessage 的第二个参数也可以发送消息,那它是否也可以当成 send 或者 invoke 来用呢?这里我测试了一下,发现主进程那边不论是用 event.reply 还是用 event.returnValue 都不行,看来,官方还是希望我们遵循:”什么样的API就做什么样的事” ,而 ipcRenderer.postMessage ,他的主要职责就是用来发送 MessagePort 的。

并且!ipcRenderer.postMessage 只能通过 ipcMain.on 来接收到, ipcRenderer.on 是接收不到的!

这样的话,看来如果我们要将某个 port 从一个渲染进程给到另一个渲染进程还是得依靠主进程了,需要它这个 媒人 来从中做媒。但问题不大,一旦这两人连接上了,就不再需要媒人了。

同时我们发现,通过这种方式我们也可以实现渲染进程与主进程之间的互相通信了,主进程在收到 port 的时候,如果不给其他人,自己用来和渲染进程通信也可以呀。

2. 主进程与渲染进程通信案例分析
好嘞,扯了这么多,让我们先写个小 demo,来看看通过 MessagePort 主进程与渲染进程是如何通信的吧。

和之前一样,让我们确定下要做什么事:

在某个时机,渲染进程创建了两个 port 并将其中一个(名为 port1)发送给了主进程
渲染进程这边的另一个 port2 绑定监听事件
主进程接收到 port1 并将它保存下来,同时也绑定监听事件
在另一个时机渲染进程通过 port2 给主进程发送消息
下面是 demo 的时序图:

第一步、调整目录结构
由于这个案例说的是渲染进程与主进程的通信,让我们基于之前的分支 example-3 再新建一个 example-4,同时删除我们不用的窗口2,此时目录结构变为:

(example-4: https://github.com/LinDaiDai/electron-ipc-example/tree/example-4)

第二步、渲染进程提供生成 MessagePort 并发送给主进程的能力
之前提到了,做的第一件事:






在某个时机,渲染进程创建了两个 port 并将其中一个(名为 port1)发送给了主进程
这里的某个时机,我们就在页面上定义两个按钮吧:

点第一个按钮创建并发送 port
点第二个按钮给主进程发送消息
// window-one/index.html
<body>
    <h1>Window One</h1>
    <button onclick="sendPortToMain()">窗口1 postMessage 给主进程发送消息端口 port1</button>
    <button onclick="sendMessageToMain()">窗口1 通过 port2 给主进程发送消息</button>
    <script src="./renderer.js"></script>
</body>
对应的渲染进程的代码:

// window-one/render.js
const { ipcRenderer } = require('electron')

let portToMain

function sendPortToMain() {
  // 1、创建一对 port
  const { port1, port2 } = new MessageChannel()
  // 2、给主进程传输消息端口 por1
  ipcRenderer.postMessage(
    'render-post-message-to-main',
    '我是渲染进程一通过 ipcRenderer.postMessage 发送过来的',
    [port1],
  )
  // 3、把 port2 赋值给 portToMain,方便其他模块获取
  portToMain = port2
  // 4、port2 绑定事件监听,之后主进程发送的消息都会在这里接收到
  portToMain.onmessage = (event) => {
    const data = event.data
    console.log('[Renderer receive]message', data)
  }
}

function sendMessageToMain() {
  portToMain.postMessage('我是渲染进程一通过传声筒 port 发送过来的')
}
在上面代码中,点击第一个按钮执行 sendPortToMain 方法,其中创建了一堆 port ,并将其中一个通过 ipcRenderer.postMessage 发送给了主进程,同时设置监听。

点击第二个按钮,到时候会执行 sendMessageToMain 方法,就可以利用 port 进行通信了。

第三步、主进程提供接收 port 和设置监听的能力
在第二步中,渲染进程发送了 port 给主进程,那么主进程这边肯定要设置一个地方去接收,接收后同时也要保证它和渲染进程后续能持续通信。

那么只需要如下处理:

// main/ipc.js
const { ipcMain } = require('electron')

// 1、主进程监听一个事件,渲染进程想要发送 port 的话,就能在这里获取到
ipcMain.on('render-post-message-to-main', (event, params) => {
  console.log('[Main receive]render-post-message-to-main', params)

  // 2、获取到 port1
  const port1 = event.ports[0]

  // 3、需要调用一下 port1 的 start()
  port1.start()

  // 4、port1 绑定事件监听,之后渲染进程一发送的消息都会在这里接收到
  port1.on('message', (event) => {
    const data = event.data
    console.log('[Main receive]message', data)

    port1.postMessage('我是主进程通过 port 回复的消息')
  })
})
(记得将 main/ipc.js 在主进程中引用一下哦)

// main/index.js
const ipc = require('./ipc')

// ...其他代码
在上面的代码中,我们先用 ipcMain 保证能接收到渲染进程发送过来的 port ,再调用 port1.start() ,然后给它绑定 message 事件,之后渲染进程一发送过来消息都能接收到,也能通过 port1 给渲染进程一发。

第四步、效果演示
一切代码准备就绪,让我们启动项目来看看效果。

分别点击窗口中的第一个和第二个按钮,能够看到主进程和渲染进程的打印日志:

(终端里主进程的打印中文会乱码,还请理解…)

3. 渲染进程与渲染进程案例预告
在看完了上面的案例之后,相信你对于这种用 postMessage 进行窗口间通信的方式有了一些了解。实际的开发场景中,我们可能还会进行渲染进程与渲染进程间的通信,甚至是同一个进程内部之间的通信。在后面的文章中,我们会介绍如何通过 postMessage 来实现一个比较通用的 electron ipc 通信的库,你也可以先利用上面的知识自己尝试着看看可以如何去写,敬请期待哦。



作者:LinDaiDai

欢迎关注微信公众号 :前端晚间课

更多文章,收录于小程序-互联网小兵