惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

有赞技术团队
有赞技术团队
B
Blog
IT之家
IT之家
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
人人都是产品经理
人人都是产品经理
博客园 - 聂微东
量子位
博客园 - 叶小钗
T
Tailwind CSS Blog
小众软件
小众软件
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - Franky
雷峰网
雷峰网
博客园 - 三生石上(FineUI控件)
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Blog — PlanetScale
Blog — PlanetScale
V
V2EX
博客园_首页
I
InfoQ
B
Blog RSS Feed
Microsoft Azure Blog
Microsoft Azure Blog

博客园 - liulun

开源自荐:功能齐全的截图软件(仅1M) 开源自荐:功能齐全的截图工具(含录屏、截长图,体积仅780KB) 自荐:开源截图工具ScreenCapture:超多控制指令,支持截长图 脚踏esbuild祥云,胸怀tsx利刃,身披scss羽衣,追寻前端的本质 开源项目自荐:截图工具(小、快、功能丰富) 如何开发Vite3插件构建Electron开发环境 Electron是什么以及可以做什么 线上Electron应用具备哪些特征? 桌面软件开发框架大赏 如何把Electron做成一个Runtime,让多个应用共享同一个Electron 微软要放弃Electron了???聊聊WebView2 一个Electron的设计缺陷及应对方案 如何点击穿透Electron不规则窗体的透明区域 Electron团队为什么要干掉remote模块 谈Vite在Electron环境下吃花卷拉馒头的现象 公司只提供签名服务,不提供证书文件,如何打包Electron应用 vue3微前端架构——基于蚂蚁qiankun框架 Vite ❤ Electron——基于Vite搭建Electron+Vue3的开发环境【一】 Electron安装过程深入解析(读完此文解决Electron安装失败导致的无法启动,无法打包的问题) 大型Electron应用本地数据库技术选型 极客手中的利器Electron
自己实现一个Electron跨进程消息组件(兼新书自荐)
liulun · 2021-12-21 · via 博客园 - liulun

我们知道开发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内部

这个组件淋漓尽致的体现了那句话:把简单、幸福留给用户;把复杂、无奈留给自己;

下面是我写的新书,这篇文章就提炼自这本书里的部分章节