自己實現一個Electron跨進程消息組件
- 2021 年 12 月 21 日
- 筆記
- javascript/jQuery/ExtJs
我們知道開發Electron應用,難免要涉及到跨進程通訊,以前Electron內置了remote模組,極大的簡化了跨進程通訊的開發工作,但這也帶來了很多問題,具體的細節請參與我之前寫的文章:
Electron團隊把remote模組拿掉之後,開發者就只能使用ipcRenderer,ipcMain,webContents等模組收發跨進程消息了,這並沒有什麼問題,但寫起來非常麻煩,跨進程消息多了之後,也很難管理維護。這就促使著我們思考如何實現一個大一統的跨進程事件組件。下面我就介紹一種方法。
首先這個組件整合了NodeJs的events模組和Electron收發事件的模組,所以先把這些模組引入進來
let events = require('events')
let { ipcRenderer, ipcMain, webContents } = require('electron')
我們假定這個組件的類名為Eventer,我們在這個類的構造函數中,實例化了一個EventEmitter對象,讓它來負責監聽和發射事件。
constructor() { this.instance = new events.EventEmitter() //this.instance.setMaxListeners(60) //Infinity this.initEventPipe() }
首先,無論是渲染進程還是主進程使用這個模組,都會執行這個構造函數,創建一個EventEmitter對象;但渲染進程的EventEmitter對象與主進程的EventEmitter對象是不同的;不同渲染進程間的EventEmitter對象也是不同的,但同一個進程內的EventEmitter對象是相同的,共享同一個EventEmitter對象,這裡我們用到了單例模式,是通過下面這行程式碼實現的:
export let eventer = new Eventer()
也就是說某個進程第一次import這個組件的時候,Eventer類就實例化了,它的構造函數就執行過了,無論這個進程再import多少次這個類,都是引用的同一個eventer對象,這個類在同一個進程內不會被實例化多次。
默認情況下EventEmitter實例最多可為任何單個事件註冊10個監聽器,如果你嫌這個數量太少,可以通過setMaxListeners方法把這個數字設置大一些,設置為Infinity就沒有任何數量限制了,但盡量不要這麼做,要不然某個事件被反覆註冊了,你也不知道。
接下來我們就在initEventPipe方法內初始化了我們自己的跨進程消息管道
private initEventPipe() { if (ipcRenderer) { ipcRenderer.on('__eventPipe', (e: Electron.IpcRendererEvent, { eventName, eventArgs }) => { this.instance.emit(eventName, e, eventArgs) }) } else if (ipcMain) { ipcMain.handle('__eventPipe', (e: Electron.IpcMainInvokeEvent, { eventName, eventArgs, broadcast }) => { this.instance.emit(eventName, e, eventArgs) if (!broadcast) return webContents.getAllWebContents().forEach((wc) => { if (wc.id != e.sender.id) { wc.send('__eventPipe', { eventName, eventArgs }) } }) }) } }
在這個方法內,我們通過ipcRenderer、ipcMain是否存在來判斷當前進程是渲染進程還是主進程;
如果是渲染進程則用ipcRenderer監聽一個名為__eventPipe的消息;如果是主進程我們則通過ipcMain監聽一個名為__eventPipe的消息。
無論是哪個進程,處理這個消息的回調函數都有兩個參數,第一個參數是Electron為跨進程消息提供的消息體,第二個參數,是我們自己構造的(後面我們會講),他們結構是相同的,都具有eventName和eventArgs屬性;
在這個回調函數中,我們在當前進程的EventEmitter對象上發射一個事件,這個事件的名字就是eventName屬性的值,事件有兩個參數,一個是Electron為跨進程消息提供的消息體,另一個是eventArgs對應的值。
如果當前進程是主進程,我們還會進一步判斷是不是有broadcast屬性,如果有,那麼就繼續給所有其他的webContents發送__eventPipe消息,消息體是由eventName和eventArgs兩個屬性組成的。
這裡我們通過e.sender.id來判斷消息是從哪個渲染進程發來的,當轉發這個消息給其他webContents時,要排除掉那個發來消息的webContents。
接下來我們看一下與事件發射有關的一系列方法
emitInProcess(eventName: string, eventArgs?: any) { this.instance.emit(eventName, eventArgs) }
這個方法在當前進程的EventEmitter對象上發射事件。它最簡單了,不多做介紹。
emitCrossProcess(eventName: string, eventArgs?: any) { if (ipcMain) { webContents.getAllWebContents().forEach((wc) => { wc.send('__eventPipe', { eventName, eventArgs }) }) } else if (ipcRenderer) { ipcRenderer.invoke('__eventPipe', { eventName, eventArgs }) } }
這個方法發射一個跨進程消息,如果是渲染進程調用這個方法,那麼消息就是發送給主進程的,如果是主進程調用這個方法,那麼消息就是發送給所有的渲染進程的。
消息的名字就是__eventPipe,消息體是eventName, eventArgs兩個參數組成的對象,我們前面講的initEventPipe方法內有監聽這個消息的邏輯。
emitToAllProcess(eventName: string, eventArgs?: any) { this.instance.emit(eventName, eventArgs) if (ipcMain) { webContents.getAllWebContents().forEach((wc) => { wc.send('__eventPipe', { eventName, eventArgs }) }) } else if (ipcRenderer) { ipcRenderer.invoke('__eventPipe', { eventName, eventArgs, broadcast: true }) } }
這個方法可以把消息發送給所有進程,首先是在自己的進程上發射eventName事件,接著判斷當前進程是主進程還是渲染進程,如果是主進程則給所有渲染進程發送消息,如果是渲染進程,則給主進程發送消息,給主進程發消息時,附加了broadcast標記。要求主進程給其他所有的渲染進程轉發消息。
emitToWebContents(wcIdOrWc: number | WebContents, eventName: string, eventArgs?: any) { if (ipcMain) { if (typeof wcIdOrWc == 'number') { webContents.getAllWebContents().forEach((wc) => { if (wc.id === wcIdOrWc) wc.send('__eventPipe', { eventName, eventArgs }) }) } else { wcIdOrWc.send('__eventPipe', { eventName, eventArgs }) } } else if (ipcRenderer) { ipcRenderer.sendTo(wcIdOrWc as number, '__eventPipe', { eventName, eventArgs }) } }
這個方法把消息發送給指定的WebContents對象,如果當前進程是主進程,則找到WebContents對象,並調用它的send方法發送消息;如果當前進程是渲染進程,則使用ipcRenderer的sendTo方法發送給目標WebContents對象。
接下來還有幾個註冊事件和取消註冊的方法
on(eventName: string, callBack: (e: any, eventArgs: any) => void) { this.instance.on(eventName, callBack) } once(eventName: string, callBack: (e: any, eventArgs: any) => void) { this.instance.once(eventName, callBack) } off(eventName: string, callBack: (e: any, eventArgs: any) => void) { if (callBack) { this.instance.removeListener(eventName, callBack) } else { this.instance.removeAllListeners(eventName) } }
這些我們就不多做解釋了。
遺留問題:我們沒辦法通過這個組件把消息透傳到子頁面iframe內部
這個組件淋漓盡致的體現了那句話:把簡單、幸福留給用戶;把複雜、無奈留給自己;
下面是我寫的新書,這篇文章就提煉自這本書里的部分章節