使用ipc为 electron 实现一个无用的通信轮子

535 阅读4分钟

入门学习electron时了解到,渲染进程与主进程存在一对多的关系, 渲染进程与主进程需要通过ipc或remote进行通信。因为ipc的使用类似websocket, 发送和监听分离到两个独立的接口且渲染进程与主进程的接口存在不小的差异。 所以使用时存在几个比较麻烦的点: 1. 对于类似http的请求模式,接收和发送逻辑存在割裂。 2. 手动指定各个窗口发送比较繁琐。 所以打算尝试通过ipc封装一个无用的通信轮子。

简单例子

  
  // 渲染端
  const renderSocket = new RenderSocket()
  renderSocket.open()
  renderSocket.on('getWinId', (pack) => {
     randerSocket.winId = pack.body.data
     renderSocket.send('getWinIdBack', { data: 'success' })
  })
  
  // 主进程
  const mainSocket = new MainSocket()
  mainSocket.open()
  const _win = new BrowserWindow({...windowConfig})
  _win.loadUrl(url)
  _win.show()
  
  mainSocket.addWind(_win)
  
  _win.webContents.on('dom-ready', () => {
      
      setTimeout(() => {
          mainSocket.mixSend('getWinId', {
              winIds: [_win.id],
              data: _win.id
          }).then(pack => {
              console.log(pack.body)
          })
      }, 1000)
      
  })
    

实现

简单路由


/**
 * 简单路由
 * @summary 使用订阅模式,实现简单路由分发
 * @function add 添加订阅
 * @function remove 移除订阅`
 * @function has 判断路径是否已注册
 * @function emit 触发订阅
 */
class LowRouter{
  
  static create(){
    return new LowRouter()
  }
  
  constructor(){
    this.handlers = new Map([])
  }
  
  add(key, handler){
    if(this.handlers.has(key)){
      const h = this.handlers.get(key) 
      h.push(handler)
    }else{
      this.handlers.set(key, [handler])
    }

    return this
  }

  remove(key){
    this.handlers.has(key) && this.handlers.delete(key)
    return this
  }

  has(key){
    return this.handlers.has(key)
  }

  emit(key, ...args){
    if(!this.handlers.has(key)){
      console.error('ERROR: 未找到路由配置', key)
      return null
    }
    
    this.handlers.get(key).forEach((handler) => {
      handler(...args)
    })
    return this
  }

}

通信包


/**
 * 通信包
 * @summary 
 * 定义Socket通信的数据包格式
 * @prop data 消息数据
 * @prop status 状态码
 * @prop statusText 状态提示
 * @prop backKey Promise标识
 * @prop sendKey Promise标识
 * @prop ...other 自定义属性
 * 
 */
class Pack{

  constructor(options){

    if(!options.target){
      throw new Error('发送地址不能为空')
    }
    
    // 添加默认值
    const _opt = {
      data: null,
      status: null,
      statusText: null,
      backKey: null,
      sendKey: this.createWaitKey(),
      ...options,
    }

    const {
      target,
      data,
      status,
      statusText,
      sendKey,
      backKey,
      ...other
    } = _opt
    
    this.target = target
    this.data = data
    this.status = status 
    this.statusText = statusText
    this.sendKey = sendKey
    this.backKey = backKey
    this.other = other
    
  }

  /**
   * 构建通信包
   */
  build(){
    return {
      body: {
        status: this.status,
        statusText: this.statusText,
        data:  this.data
      },
      target: this.target,
      sendKey: this.sendKey,
      backKey: this.backKey,
      ...this.other
    }
  }

  // 生成唯一标识, 供mixSend类请求回调查询 
  createWaitKey(){
    return new Date().getTime()
  }
}

基础通信对象

/**
 * 通信对象
 * @summary 抽离公共通信模型,向外开发通用通信接口
 * @props router 回调路由
 * @props ipc 抽象通信对象
 * @props waitQueue 回调队列, 通过将对ipc包装为Promise, 实现类似http请求的调用方式
 * @function open 开启通信监听
 * @function close 关闭通信监听
 * @function createPack 创建通信包
 * @function send 普通发送, 不会向 waitQueue 添加返回处理
 * @function mixSend 返回Pomise, 类http请求的发送, 会向 waitQueue 添加返回处理
 * 
 * @example
 * const socket1 = new Socket(router, ipc)
 * // 开启监听
 * socket1.open
 * // 挂载监听函数
 * socket2.on('getBack', (pack) => {
 *    // 输出信息
 *    console.log(pack.body.data)
 * })
 * 
 * const socket2 = new Socket(router, ipc)
 * socket2.open()
 * socket2.on('getSend', (pack) => {
 *   // 接收信息
 *  console.log(pack.status, pack.body.data)
 *  // 消息回复
 *  socket2.send({data: 'back message'}, pack)
 * })
 * 
 * // 初始发送
 * socket1.mixSend({ data: 'start message' }).then(pack => {
 *  // 类似 http 的消息回调接收
 *  console.log('promise back', pack.body.data)
 * })
 * 
 */
class Socket{
  
  /**
   * 
   * @param router 路由对象, 需要实现 { emit: 触发回调, add: 添加回调 } 接口 
   * @param ipc 抽象通信对象, 
   */
  constructor(router, ipc){
    this.router = router
    this.ipc = ipc
    this.waitQueue = new Map([])
  }


  open(){
    const _this = this
    _this.ipc.onMessage(pack =>{
      // 取出目标地址
      const { target } = pack
      const _waitKey = pack.backKey

      // 查询是否存在发送回调
      if(_waitKey && _this.waitQueue.has(_waitKey)){
        const _wait = _this.waitQueue.get(_waitKey)
        _this.waitQueue.delete(_waitKey)
        // TODO 这里只做了浅拷贝,
        // promise 回调与路由回调都将获取到pack,
        // 存在深拷贝问题
        _wait.resolve({...pack})
      }
      
      // 触发路由分发
      if(_this.router.has(target)){
        _this.router.emit(target, {...pack})
      }

    })
  }

  close(){
    _this.ipc.close()
  }

  /**
   * oldPack 便于提取返回标识信息, 以供 mixSend 类型的发送,获取返回回调  
   */
  createPack(options, oldPack = {target: null, sendKey: null}){
    const _opt = {
      target: oldPack.target,
      // 发送方的发送key【sendKey】, 就是返送后回调的取值key【backKey】
      backKey: oldPack.sendKey,
      ...options,
    }
    return new Pack(_opt)
  }
  

  // 普通发送
  send(options, oldPack){
    const _pack = new Pack(this.createPack(options, oldPack))
    this.ipc.send(_pack.build())
  }

  // 混合发送, 返回Promise
  mixSend(options, oldPack){

    const _this = this
    const _wait = {}
    const _pack = _this.createPack(options, oldPack)
    
    // 将发送包装为 promise
    _wait.promiseSend = new Promise((resolve, reject) => {
      _wait.resolve = resolve;
      _wait.reject = reject;
      _this.ipc.send(_pack.build())
    })

    // 追加到等待队列
    _this.waitQueue.set(_pack.sendKey, _wait)
    return _wait.promiseSend
  }
  
  on(path, callback){
    this.router.add(path, (ctx) => callback && callback(ctx))
  }
}

electron ipc兼容


/**
 * 渲染端Socket封装
 * @summary 
 * 封装渲染端的特型的Socket对象,
 * 1.将 ipcRender 接口兼容到Socket中.
 * 2.添加特定标识【winId】的添加与解析
 * 
 * @param winid 窗口id, 主进程需要该id判断需要调用的回复窗口
 * @param router 回调路由
 * @param listenKey 渲染端主通信口,监听标识. 也是主进程端的发送标识
 * @param sendKey 主进程端主通信接口, 监听标识. 也是渲染端的发送标识
 */
class RendSocket{

  constructor(winId, router, listenKey = mainToRender, sendKey = renderToMain){
    this.winId = winId

    // 接口兼容, 包装渲染进程 ipc
    this.ipc = {
      send: (message) => ipcRenderer.send(sendKey, message),
      onMessage: (callback) => ipcRenderer.on(listenKey, (event, data) => callback({event, ...data})),
      close: () => ipcRenderer.removeAllListeners(listenKey)
    }
    
    this.socket = new Socket(router || LowRouter.create(), this.ipc)
  }
  
  send(path, message, oldPack){
    this.socket.send({target: path, ...message, winId: this.winId}, oldPack)
    return this
  }

  mixSend(path, message){
    return this.socket.mixSend({target: path, ...message, winId: this.winId})
  }

  on(path, callback){
    this.socket.on(path, callback)
    return this
  }

  open(){
    this.socket.open()
    return this
  }

  close(){
    this.socket.close()
    return this
  }
}


/**
 * 主进程Socket封装
 * @summary
 * 与RendSocket类似, 将主进程通信 ipcMain 兼容到Socket中
 * 主要区别在于发送接口存在不同,应为页面进程与主进程存一对多的关系, 
 * 主进程需要通过具体的 window.webContext 发送数据到指定的窗口。
 * 所以从主进程端发送数据存在, 一对一, 一对多, 全广播 的模式。
 * 这里通过缓存window对象,并在通信是传递winId实现对发送窗口的识别。
 * 
 * @param router 回调路由
 * @param listenKey  主进程端主通信口,监听标识. 也是渲染端的发送标识
 * @param sendKey 渲染端主通信接口,监听标识. 也是主进程端的发送标识
 */
class MainSocket{
  constructor(router, listenKey = renderToMain, sendKey = mainToRender){
    const _this = this
    this.winds = new Map([])
    this.ipc = {
      onMessage: (callback) => ipcMain.on(listenKey, (event, data) => callback({ event, ...data })),
      send: (pack) => {
        const { winIds, ..._pack } = pack

        // 筛选目标窗口
        const winds = winIds ? winIds.reduce((acc, next) => {
          if(_this.winds.has(next)){
            return [...acc, _this.winds.get(next)]
          }
          return acc
        }, []) : [..._this.winds.values()]
        
        // 向各窗口分发消息
        winds.forEach(win => {
          win.webContents.send(sendKey, _pack)
        })
      },

      close: () => ipcMain.removeAllListeners(listenKey)
    }
    
    this.socket = new Socket(router || LowRouter.create(), this.ipc)
  }

  // 缓存窗口对象
  addWind(wind){
    this.winds.set(wind.id, wind)
    return this
  }

  removeWind(wind){
    this.winds.has(wind.id) && this.winds.delete(wind.id)
    return this
  }

  send(path, message, oldPack){
    this.socket.send({target: path, ...message}, oldPack)
    return this
  }

  mixSend(path, message){
    return this.socket.mixSend({target: path, ...message})
  }

  on(path, callback){
    this.socket.on(path, callback)
    return this
  }

  open(){
    this.socket.open()
    return this
  }

  close(){
    this.socket.close()
    return this
  }
}