自己實現一個Electron跨程式訊息元件

liulun發表於2021-12-21

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

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

 

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

 

 

 

相關文章