程式間通訊(IPC)並非僅限於 Electron,而是源自甚至早於 Unix 誕生的概念。儘管“程式間通訊”這個術語的確創造於何時並不清楚,但將資料傳遞給另一個程式或程式的理念可以追溯至 1964 年,當時 Douglas McIlroy 在 Unix 的第三版(1973 年)中描述了 Unix 管道的概念。
We should have some ways of coupling programs like garden hose--screw in another segment when it becomes when it becomes necessary to massage data in another way.
例如,我們可以透過使用管道運算子(|)將一個程式的輸出傳遞到另一個程式。
# 列出當前目錄下的所有.ts檔案
ls | grep .ts
在 Unix 系統中,管道只是 IPC 的一種形式,還有許多其他形式,比如訊號、訊息佇列、訊號量和共享記憶體。
一、ipcMain 和 ipcRenderer
與 Chromium 相同,Electron 使用程式間通訊(IPC)來在程式之間進行通訊,在介紹 Electron 程式間通訊前,我們必須先認識一下 Electron 的 2 個模組。
- ipcMain 是一個僅在主程式中以非同步方式工作的模組,用於與渲染程式交換訊息。
- ipcRenderer 是一個僅在渲染程式中以非同步方式工作的模組,用於與主程式交換訊息。
ipcMain 和 ipcRenderer 是 Electron 中負責通訊的兩個主要模組。它們繼承自 NodeJS 的 EventEmitter 模組。在 EventEmitter 中允許我們向指定 channel 傳送訊息。channel 是一個字串,在 Electron 中 ipcMain 和 ipcRenderer 使用它來發出和接收事件/資料。
// 接受訊息
// EventEmitter: ipcMain / ipcRenderer
EventEmitter.on("string", function callback(event, messsage) {});
// 傳送訊息
// EventEmitter: win.webContents / ipcRenderer
EventEmitter.send("string", "mydata");
二、渲染程式 -> 主程式
大多數情況下的通訊都是從渲染程式到主程式,渲染程式依賴 ipcRenderer 模組給主程式傳送訊息,官方提供了三個方法:
- ipcRenderer.send(channel, ...args)
- ipcRenderer.invoke(channel, ...args)
- ipcRenderer.sendSync(channel, ...args)
channel 表示的就是事件名(訊息名稱), args 是引數。需要注意的是引數將使用結構化克隆演算法進行序列化,就像瀏覽器的 window.postMessage 一樣,因此不會包含原型鏈。傳送函式、Promise、Symbol、WeakMap 或 WeakSet 將會丟擲異常。
2.1 ipcRenderer.send
渲染程式透過 ipcRenderer.send 傳送訊息:
// render.js
import { ipcRenderer } from 'electron';
function sendMessageToMain() {
ipcRenderer.send('my_channel', 'my_data');
}
主程式透過 ipcMain.on 來接收訊息:
// main.js
import { ipcMain } from 'electron';
ipcMain.on('my_channel', (event, message) => {
console.log(`receive message from render: ${message}`)
})
請注意,如果使用 send 來傳送資料,如果你的主程式需要回復訊息,那麼需要使用 event.replay 來進行回覆:
// main.js
import { ipcMain } from 'electron';
ipcMain.on('my_channel', (event, message) => {
console.log(`receive message from render: ${message}`)
event.reply('reply', 'main_data')
})
同時,渲染程式需要進行額外的監聽:
// renderer.js
ipcRenderer.on('reply', (event, message) => {
console.log('replyMessage', message);
})
2.2 ipcRenderer.invoke
渲染程式透過 ipcRenderer.invoke 傳送訊息:
// render.js
import { ipcRenderer } from 'electron';
async function invokeMessageToMain() {
const replyMessage = await ipcRenderer.invoke('my_channel', 'my_data');
console.log('replyMessage', replyMessage);
}
主程式透過 ipcMain.handle 來接收訊息:
// main.js
import { ipcMain } from 'electron';
ipcMain.handle('my_channel', async (event, message) => {
console.log(`receive message from render: ${message}`);
return 'replay';
});
注意,渲染程式透過 ipcRenderer.invoke 傳送訊息後,invoke 的返回值是一個 Promise<pending> 。主程式回覆訊息需要透過 return 的方式進行回覆,而 ipcRenderer 只需要等到 Promise resolve 即可獲取到返回的值。
2.3 ipcRender.sendSync
渲染程式透過 ipcRender.sendSync 來傳送訊息:
// render.js
import { ipcRenderer } from 'electron';
async function sendSyncMessageToMain() {
const replyMessage = await ipcRenderer.sendSync('my_channel', 'my_data');
console.log('replyMessage', replyMessage);
}
主程式透過 ipcMain.on 來接收訊息:
// main.js
import { ipcMain } from 'electron';
ipcMain.on('my_channel', async (event, message) => {
console.log(`receive message from render: ${message}`);
event.returnValue = 'replay';
});
注意,渲染程式透過 ipcRenderer.sendSync 傳送訊息後,主程式回覆訊息需要透過 e.returnValue 的方式進行回覆,如果 event.returnValue 不為 undefined 的話,渲染程式會等待 sendSync 的返回值才執行後面的程式碼。
2.4 小結
上面我們介紹了從渲染程式到主程式的幾個通訊方法,總結如下。
- ipcRenderer.send: 這個方法是非同步的,用於從渲染程式向主程式傳送訊息。它傳送訊息後不會等待主程式的響應,而是立即返回,適合在不需要等待主程式響應的情況下傳送訊息。
- ipcRenderer.sendSync: 與 ipcRenderer.send 不同,這個方法是同步的,也是用於從渲染程式向主程式傳送訊息,但是它會等待主程式返回響應。它會阻塞當前程式,直到收到主程式的返回值或者超時。
- ipcRenderer.invoke: 這個方法也是用於從渲染程式向主程式傳送訊息,但是它是一個非同步的方法,可以方便地在渲染程式中等待主程式返回 Promise 結果。相對於 send 和 sendSync,它更適合處理非同步操作,例如主程式返回 Promise 的情況。
三、主程式 -> 渲染程式
主程式向渲染程式傳送訊息一種方式是當渲染程式透過 ipcRenderer.send、ipcRenderer.sendSync、ipcRenderer.invoke 向主程式傳送訊息時,主程式透過 event.replay、event.returnValue、return ... 的方式進行傳送。這種方式是被動的,需要等待渲染程式先建立訊息推送機制,主程式才能進行回覆。
其實除了上面說的幾種被動接收訊息的模式進行推送外,還可以透過 webContents 模組進行訊息通訊。
3.1 ipcMain 和 webContents
主程式使用 ipcMain 模組來監聽來自渲染程式的事件,透過 event.sender.send() 方法向渲染程式傳送訊息。
// 主程式
import { ipcMain, BrowserWindow } from 'electron';
ipcMain.on('messageFromMain', (event, arg) => {
event.sender.send('messageToRenderer', 'Hello from Main!');
});
3.2 BrowserWindow.webContents.send
BrowserWindow.webContents.send 可以在主程式中直接使用 BrowserWindow 物件的 webContents.send() 方法向渲染程式傳送訊息。
// 主程式
import { BrowserWindow } from 'electron';
const mainWindow = new BrowserWindow();
mainWindow.loadFile('index.html');
// 在某個事件或條件下傳送訊息
mainWindow.webContents.send('messageToRenderer', 'Hello from Main!');
3.3 小結
不管是透過 event.sender.send() 還是 BrowserWindow.webContents.send 的方式,如果你只是單視窗的資料通訊,那麼本質上是沒什麼差異的。但是如果你想要傳送一些資料到特定的視窗,那麼你可以直接使用 BrowserWindow.webContents.send 這種方式。
四、渲染程式 -> 渲染程式
預設情況下,渲染程式和渲染程式之間是無法直接進行通訊的。
雖然說無法直接通訊,但是還是有一些“曲線救國”的方式。
4.1 利用主程式作為中間人
首先,需要在主程式註冊一個事件監聽程式,監聽來自渲染程式的事件:
// main.js
// window 1
function createWindow1 () {
window1 = new BrowserWindow({width: 800,height: 600})
window1.loadURL('window1.html')
window1.on('closed', function () {
window1 = null
})
return window1
}
// window 2
function createWindow2 () {
window2 = new BrowserWindow({width: 800, height: 600})
window2.loadURL('window2.html')
window2.on('closed', function () {
window2 = null
})
return window2
}
app.on('ready', () => {
createWindow1();
createWindow2();
ipcMain.on('win1-msg', (event, arg) => {
// 這條訊息來自 window 1
console.log("name inside main process is: ", arg);
// 傳送給 window 2 的訊息.
window2.webContents.send( 'forWin2', arg );
});
})
然後,在 window2 視窗建立一個監聽事件:
ipcRenderer.on('forWin2', function (event, arg){
console.log(arg);
});
這樣,window1 傳送的 win1-msg 事件,就可以傳輸到 window2:
ipcRenderer.send('win1-msg', 'msg from win1');
4.2 使用 MessagePort
上面的傳輸方式雖然可以實現渲染程式之間的通訊,但是非常依賴主程式,寫起來也比較麻煩,那有什麼不依賴於主程式的方式嘛?那當然也是有的,那就是 MessagePort。
MessagePort 並不是 Electron 提供的能力,而是基於 MDN 的 Web 標準 API,這意味著它可以在渲染程式直接建立。同時 Electron 提供了 node.js 側的實現,所以它也能在主程式建立。
接下來,我們將透過一個示例來描述如何透過 MessagePort 來實現渲染程式之間的通訊。
4.2.1 主程式中建立 MessagePort
import { BrowserWindow, app, MessageChannelMain } from 'electron';
app.whenReady().then(async () => {
// 建立視窗
const mainWindow = new BrowserWindow({
show: false,
webPreferences: {
contextIsolation: false,
preload: 'preloadMain.js'
}
})
const secondaryWindow = new BrowserWindow({
show: false,
webPreferences: {
contextIsolation: false,
preload: 'preloadSecondary.js'
}
})
// 建立通道
const { port1, port2 } = new MessageChannelMain()
// webContents準備就緒後,使用postMessage向每個webContents傳送一個埠。
mainWindow.once('ready-to-show', () => {
mainWindow.webContents.postMessage('port', null, [port1])
})
secondaryWindow.once('ready-to-show', () => {
secondaryWindow.webContents.postMessage('port', null, [port2])
})
})
例項化 MessageChannel 類之後,就產生了兩個 port: port1 和 port2。接下來只要讓 渲染程式1 拿到 port1、渲染程式2 拿到 port2,那麼現在這兩個程式就可以透過 port.onmessage 和 port.postMessage 來收發彼此間的訊息了。如下:
// mainWindow
port1.onmessage = (event) => {
console.log('received result:', event.data)
};
port1.postMessage('我是渲染程式一傳送的訊息');
// secondaryWindow
port2.onmessage = (event) => {
console.log('received result:', event.data)
};
port2.postMessage('我是渲染程式二傳送的訊息');
4.2.2 渲染程式中獲取 port
有了上面的知識,我們最重要的任務就是需要獲取主程式中建立的 port 物件,要做的是在你的預載入指令碼(preload.js)中透過 IPC 接收 port,並設定相應的監聽器。
// preloadMain.js
// preloadSecondary.js
const { ipcRenderer } = require('electron')
ipcRenderer.on('port', e => {
// 接收到埠,使其全域性可用。
window.electronMessagePort = e.ports[0]
window.electronMessagePort.onmessage = messageEvent => {
// 處理訊息
}
})
4.3 訊息通訊
透過上面的一些操作後,就可以在應用程式的任何地方呼叫 postMessage 方法向另一個渲染程式傳送訊息。
// mainWindow renderer.js
// 在 renderer 的任何地方都可以呼叫 postMessage 向另一個程式傳送訊息
window.electronMessagePort.postMessage('ping')