大概是全網(wǎng)最詳細(xì)的Electron ipc 講解(三)——定情信物傳聲筒port
以下文章來(lái)源于LinDaiDai ,作者LinDaiDai
文章
《大概是全網(wǎng)最詳細(xì)的Electron ipc 講解(一)——主進(jìn)程與渲染進(jìn)程的兩情相悅》,
《大概是全網(wǎng)最詳細(xì)的Electron ipc 講解(二)——渲染進(jìn)程與渲染進(jìn)程的搭橋牽線(xiàn)》
前言
本系列共有以下幾個(gè)章節(jié):
主進(jìn)程與渲染進(jìn)程的兩情相悅
渲染進(jìn)程與渲染進(jìn)程的搭橋牽線(xiàn)
定情信物傳聲筒port
您此次閱讀的是第三章節(jié):定情信物傳聲筒port。
注:以上所有文章都被歸檔到:https://github.com/LinDaiDai/niubility-coding-js 中 ,案例都上傳至:https://github.com/LinDaiDai/electron-ipc-example ,歡迎 Star,感謝 Star。
大綱
在之前的文章中,我們主要介紹了 ipcMain 和 ipcRenderer 是如何實(shí)現(xiàn)主進(jìn)程與渲染進(jìn)程、以及渲染進(jìn)程與渲染進(jìn)程進(jìn)行通信的。大家不難發(fā)現(xiàn),之前介紹的方式都非常依賴(lài)主進(jìn)程,特別是渲染進(jìn)程之間的通信,每次都需要主進(jìn)程這個(gè)中間人來(lái)傳話(huà),難道就沒(méi)有什么更簡(jiǎn)單點(diǎn)的方式嗎?
咦,這還真有一個(gè),那就是利用 MessagePort ,通俗易懂的翻譯過(guò)來(lái):消息端口...(啪,就你英語(yǔ)好是吧)
大家別急,還不知道是啥玩意的話(huà),讓我來(lái)給大家介紹一下。用個(gè)小故事來(lái)簡(jiǎn)單舉個(gè)例子哈:
渲染進(jìn)程一(某先生)和渲染進(jìn)程二(某女士)通過(guò)媒人介紹認(rèn)識(shí),由于男俊女美且三觀相符,很快兩人就看對(duì)眼墜入愛(ài)河了,但是奈何工作地點(diǎn)不在一起,不得不異地。這剛認(rèn)識(shí)還處于曖昧期的倆年輕人咋能忍住不聯(lián)系呢。于是互相交換了手機(jī)號(hào)加上了wx,相親結(jié)束后,每日通過(guò)手機(jī)互相聯(lián)系,增進(jìn)感情,不日,便確定關(guān)系,訂婚,結(jié)婚,買(mǎi)房,生娃......停停停,給我回來(lái)。
咳咳咳,故事呢,其實(shí)到 "每日通過(guò)手機(jī)互相聯(lián)系" 就結(jié)束了哈,后面自行腦補(bǔ)。在上面這則小故事中,媒人就是主進(jìn)程,渲染進(jìn)程一和二在最初,會(huì)通過(guò)主進(jìn)程進(jìn)行一個(gè)交換 port 的過(guò)程,后續(xù)都通過(guò) port 來(lái)進(jìn)行通信,不再依賴(lài)主進(jìn)程了。
乍一看,是不是覺(jué)得這種通信方式比前面介紹的那些靠譜多了,而且還不需要通過(guò)主進(jìn)程中繼的性能開(kāi)銷(xiāo)。
講完了故事,聊了個(gè)大概,讓我們來(lái)看看本篇文章大綱吧:
MessagePort 的基礎(chǔ)用法
主進(jìn)程與渲染進(jìn)程通信案例分析
渲染進(jìn)程與渲染進(jìn)程案例預(yù)告
1. MessagePort 的基礎(chǔ)用法
1.1 如何創(chuàng)建 MessagePort
首先還是得先來(lái)看看 MessagePort 的基礎(chǔ)用法。MessagePort 對(duì)象的創(chuàng)建依賴(lài)于 MessageChannel 類(lèi):
const channel = new MessageChannel();
const port1 = channel.port1
const port2 = channel.port2
// 或者簡(jiǎn)寫(xiě)為:
const { port1, port2 } = new MessageChannel();
實(shí)例化 MessageChannel 類(lèi)之后,就產(chǎn)生了兩個(gè) port :port1 和 port2 。這兩個(gè) port 就是 MessagePort 對(duì)象。它就是我們上面那則故事提到的,可以用于兩個(gè)進(jìn)程之間進(jìn)行長(zhǎng)期通信的關(guān)鍵所在。
舉個(gè)小例子,假設(shè)現(xiàn)在:
渲染進(jìn)程一有了 port1
渲染進(jìn)程二有了 port2
那么現(xiàn)在這兩個(gè)進(jìn)程就可以通過(guò) port.onmessage 和 port.postMessage 來(lái)收發(fā)彼此間的消息了:
// 渲染進(jìn)程一:
port1.onmessage = (event) => {
console.log('received result:', event.data)
};
port1.postMessage('我是渲染進(jìn)程一發(fā)送的消息');
// 渲染進(jìn)程二:
port2.onmessage = (event) => {
console.log('received result:', event.data)
};
port2.postMessage('我是渲染進(jìn)程二發(fā)送的消息');
只要 port1 和 port2 一直都存在,它們就可以進(jìn)行持久通信,怎么樣,是不是很 niubility 。
OKK,那么現(xiàn)在如果是在渲染進(jìn)程一創(chuàng)建的這兩個(gè) port ,關(guān)鍵就是如何把 port2 給到另一個(gè)渲染進(jìn)程二了。也就涉及到了 MessagePort 的傳遞。
1.2 ipcRenderer.postMessage()
說(shuō)到 MessagePort 的傳遞就得談到 ipcRenderer 對(duì)象的 postMessage 方法了。因?yàn)?MessagePort 對(duì)象就是依靠它來(lái)傳遞。沒(méi)錯(cuò),此時(shí)應(yīng)該有小伙伴可能想起來(lái)了,我們平常網(wǎng)頁(yè)上的 window 對(duì)象也有一個(gè) postMessage 方法,這兩者之間其實(shí)挺像的,只不過(guò)呢,是在不同的通道上。
portMessage 它的參數(shù)如下:
ipcRenderer.postMessage(channle, message, [transfer])
channel String:事件名
message any:要傳遞的消息
transfer MessagePort[] (optional):0個(gè)或多個(gè) MessagePort 對(duì)象。
前兩個(gè)好理解,其實(shí)和 ipcRenderer 的其它方法差不多,事件,以及傳遞的消息。第三個(gè)參數(shù)有些特別,它是一個(gè)數(shù)組,其中可以傳遞 MessagePort 對(duì)象。這里需要注意,別看第三個(gè)參數(shù)標(biāo)記的是 optional ,但其實(shí)它也是需要傳遞的,如果你不需要傳 MessagePort 對(duì)象,那么就需要定義一個(gè)空數(shù)組,否則就會(huì)報(bào)錯(cuò)啦。
另外,在之前的文章中,我們還有用到 ipcRenderer 的其它方法: send、 invoke 、 sendSync ,這三種方法主進(jìn)程都是可以給渲染進(jìn)程傳遞返回結(jié)果的,比如:
// render.js
const replyMessage = await ipcRenderer.invoke('render-invoke-to-main', '我是渲染進(jìn)程通過(guò) invoke 發(fā)送的消息');
console.log('replyMessage', replyMessage); // "我是主進(jìn)程返回的消息"
postMessage 的第二個(gè)參數(shù)也可以發(fā)送消息,那它是否也可以當(dāng)成 send 或者 invoke 來(lái)用呢?這里我測(cè)試了一下,發(fā)現(xiàn)主進(jìn)程那邊不論是用 event.reply 還是用 event.returnValue 都不行,看來(lái),官方還是希望我們遵循:”什么樣的API就做什么樣的事” ,而 ipcRenderer.postMessage ,他的主要職責(zé)就是用來(lái)發(fā)送 MessagePort 的。
并且!ipcRenderer.postMessage 只能通過(guò) ipcMain.on 來(lái)接收到, ipcRenderer.on 是接收不到的!
這樣的話(huà),看來(lái)如果我們要將某個(gè) port 從一個(gè)渲染進(jìn)程給到另一個(gè)渲染進(jìn)程還是得依靠主進(jìn)程了,需要它這個(gè) 媒人 來(lái)從中做媒。但問(wèn)題不大,一旦這兩人連接上了,就不再需要媒人了。
同時(shí)我們發(fā)現(xiàn),通過(guò)這種方式我們也可以實(shí)現(xiàn)渲染進(jìn)程與主進(jìn)程之間的互相通信了,主進(jìn)程在收到 port 的時(shí)候,如果不給其他人,自己用來(lái)和渲染進(jìn)程通信也可以呀。
2. 主進(jìn)程與渲染進(jìn)程通信案例分析
好嘞,扯了這么多,讓我們先寫(xiě)個(gè)小 demo,來(lái)看看通過(guò) MessagePort 主進(jìn)程與渲染進(jìn)程是如何通信的吧。
和之前一樣,讓我們確定下要做什么事:
在某個(gè)時(shí)機(jī),渲染進(jìn)程創(chuàng)建了兩個(gè) port 并將其中一個(gè)(名為 port1)發(fā)送給了主進(jìn)程
渲染進(jìn)程這邊的另一個(gè) port2 綁定監(jiān)聽(tīng)事件
主進(jìn)程接收到 port1 并將它保存下來(lái),同時(shí)也綁定監(jiān)聽(tīng)事件
在另一個(gè)時(shí)機(jī)渲染進(jìn)程通過(guò) port2 給主進(jìn)程發(fā)送消息
下面是 demo 的時(shí)序圖:
第一步、調(diào)整目錄結(jié)構(gòu)
由于這個(gè)案例說(shuō)的是渲染進(jìn)程與主進(jìn)程的通信,讓我們基于之前的分支 example-3 再新建一個(gè) example-4,同時(shí)刪除我們不用的窗口2,此時(shí)目錄結(jié)構(gòu)變?yōu)椋?br>
(example-4: https://github.com/LinDaiDai/electron-ipc-example/tree/example-4)
第二步、渲染進(jìn)程提供生成 MessagePort 并發(fā)送給主進(jìn)程的能力
之前提到了,做的第一件事:
在某個(gè)時(shí)機(jī),渲染進(jìn)程創(chuàng)建了兩個(gè) port 并將其中一個(gè)(名為 port1)發(fā)送給了主進(jìn)程
這里的某個(gè)時(shí)機(jī),我們就在頁(yè)面上定義兩個(gè)按鈕吧:
點(diǎn)第一個(gè)按鈕創(chuàng)建并發(fā)送 port
點(diǎn)第二個(gè)按鈕給主進(jìn)程發(fā)送消息
// window-one/index.html
<body>
<h1>Window One</h1>
<button onclick="sendPortToMain()">窗口1 postMessage 給主進(jìn)程發(fā)送消息端口 port1</button>
<button onclick="sendMessageToMain()">窗口1 通過(guò) port2 給主進(jìn)程發(fā)送消息</button>
<script src="./renderer.js"></script>
</body>
對(duì)應(yīng)的渲染進(jìn)程的代碼:
// window-one/render.js
const { ipcRenderer } = require('electron')
let portToMain
function sendPortToMain() {
// 1、創(chuàng)建一對(duì) port
const { port1, port2 } = new MessageChannel()
// 2、給主進(jìn)程傳輸消息端口 por1
ipcRenderer.postMessage(
'render-post-message-to-main',
'我是渲染進(jìn)程一通過(guò) ipcRenderer.postMessage 發(fā)送過(guò)來(lái)的',
[port1],
)
// 3、把 port2 賦值給 portToMain,方便其他模塊獲取
portToMain = port2
// 4、port2 綁定事件監(jiān)聽(tīng),之后主進(jìn)程發(fā)送的消息都會(huì)在這里接收到
portToMain.onmessage = (event) => {
const data = event.data
console.log('[Renderer receive]message', data)
}
}
function sendMessageToMain() {
portToMain.postMessage('我是渲染進(jìn)程一通過(guò)傳聲筒 port 發(fā)送過(guò)來(lái)的')
}
在上面代碼中,點(diǎn)擊第一個(gè)按鈕執(zhí)行 sendPortToMain 方法,其中創(chuàng)建了一堆 port ,并將其中一個(gè)通過(guò) ipcRenderer.postMessage 發(fā)送給了主進(jìn)程,同時(shí)設(shè)置監(jiān)聽(tīng)。
點(diǎn)擊第二個(gè)按鈕,到時(shí)候會(huì)執(zhí)行 sendMessageToMain 方法,就可以利用 port 進(jìn)行通信了。
第三步、主進(jìn)程提供接收 port 和設(shè)置監(jiān)聽(tīng)的能力
在第二步中,渲染進(jìn)程發(fā)送了 port 給主進(jìn)程,那么主進(jìn)程這邊肯定要設(shè)置一個(gè)地方去接收,接收后同時(shí)也要保證它和渲染進(jìn)程后續(xù)能持續(xù)通信。
那么只需要如下處理:
// main/ipc.js
const { ipcMain } = require('electron')
// 1、主進(jìn)程監(jiān)聽(tīng)一個(gè)事件,渲染進(jìn)程想要發(fā)送 port 的話(huà),就能在這里獲取到
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、需要調(diào)用一下 port1 的 start()
port1.start()
// 4、port1 綁定事件監(jiān)聽(tīng),之后渲染進(jìn)程一發(fā)送的消息都會(huì)在這里接收到
port1.on('message', (event) => {
const data = event.data
console.log('[Main receive]message', data)
port1.postMessage('我是主進(jìn)程通過(guò) port 回復(fù)的消息')
})
})
(記得將 main/ipc.js 在主進(jìn)程中引用一下哦)
// main/index.js
const ipc = require('./ipc')
// ...其他代碼
在上面的代碼中,我們先用 ipcMain 保證能接收到渲染進(jìn)程發(fā)送過(guò)來(lái)的 port ,再調(diào)用 port1.start() ,然后給它綁定 message 事件,之后渲染進(jìn)程一發(fā)送過(guò)來(lái)消息都能接收到,也能通過(guò) port1 給渲染進(jìn)程一發(fā)。
第四步、效果演示
一切代碼準(zhǔn)備就緒,讓我們啟動(dòng)項(xiàng)目來(lái)看看效果。
分別點(diǎn)擊窗口中的第一個(gè)和第二個(gè)按鈕,能夠看到主進(jìn)程和渲染進(jìn)程的打印日志:
(終端里主進(jìn)程的打印中文會(huì)亂碼,還請(qǐng)理解…)
3. 渲染進(jìn)程與渲染進(jìn)程案例預(yù)告
在看完了上面的案例之后,相信你對(duì)于這種用 postMessage 進(jìn)行窗口間通信的方式有了一些了解。實(shí)際的開(kāi)發(fā)場(chǎng)景中,我們可能還會(huì)進(jìn)行渲染進(jìn)程與渲染進(jìn)程間的通信,甚至是同一個(gè)進(jìn)程內(nèi)部之間的通信。在后面的文章中,我們會(huì)介紹如何通過(guò) postMessage 來(lái)實(shí)現(xiàn)一個(gè)比較通用的 electron ipc 通信的庫(kù),你也可以先利用上面的知識(shí)自己嘗試著看看可以如何去寫(xiě),敬請(qǐng)期待哦。
作者:LinDaiDai
歡迎關(guān)注微信公眾號(hào) :前端晚間課
更多文章,收錄于小程序-互聯(lián)網(wǎng)小兵