自己實現一個Electron跨進程消息組件

我們知道開發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內部

這個組件淋漓盡致的體現了那句話:把簡單、幸福留給用戶;把複雜、無奈留給自己;

 

下面是我寫的新書,這篇文章就提煉自這本書里的部分章節