前言碎语

此想法是在使用 electron 进程间通信(IPC)过程中,无法忍受其 API 的使用不友好性而产生。

为了提高代码可读性、可维护性,而不得已造轮子了。

生命在于折腾,其乐无穷。

Electron 中 IPC 的通信方式

在 Electron 中分为两个进程:

  1. Main Process(主进程)。是 Node.js 跑的一个进程,可以调用 Node API 和 Electron 封装好的 API。
  2. Renderer Process(渲染进程)。是运行在 Chromium 中的 web 进程。

因为 Electron 出于安全考虑,渲染进程的 API 是有限制的。因为 web 端可能会加载第三方 js 代码,不可能让第三方为所欲为的。

假如在一些场景中我可能要获取某个文件夹下的文件列表(举例而已),在 Node 中使用 fs 模块即可完成,但是在 web 端无法使用 fs API。

这时候就需要使用 IPC 进程通信来解决。

就像是 nodejs 中的子进程一样
通过 spawn 方法开启一个子进程,两个进程之间只能通过 IPC 协议通信

主进程:



import { IpcMain } from 'electron'
import fs from 'fs'

// 监听渲染进程发来的 getDir 请求
IpcMain.on('getDir', (event, data) => {
  fs.readdir('path', (err, files) => {
     if (err) {
        // 响应渲染进程获取失败了
        event.reply('dir-result', 'err')
     } else {
        const result = files.map(/* do something */)
        // 响应渲染进程获取到的结果
        event.reply('dir-result', result)
     }
  })
})



渲染进程:



const { ipcRenderer } = require('electron')

// 先监听主进程发来的消息
ipcRenderer.on('dir-result', (event, data) => {
   // do something
})

// 发送给主进程
ipcRenderer.send('getDir', '/Users')



代码很简单,主进程先监听渲染进程消息,渲染进程再监听主进程消息,然后渲染进程发起消息。

很好理解,只不过渲染进程的操作过于繁琐

假如此逻辑放入vue 组件中,那么需要在 created 中进行监听主进程消息,在 destroyed 中进行解绑改事件。

那么怎么去优化此处的体验呢?造轮子呗~

对 IPC 通信方式进行二次封装

先看下面这段代码:

此处为渲染进程部分代码,以 React 组件举例。



// 渲染进程
import React, { useState, useEffect } from 'react'
import request from './request'

export default function IpcTest () {
  const [list, setList] = useState([])
    
  useEffect(() => {
    init()
  }, [])
  
  // 进行初始化操作
  const init = async () => {
    // request 取代 ipcRenderer.send 和 ipcRenderer.on
    const result = await request('test', { a: 1 })
    // result => ['1', '2', '3'] from Main Process
    setList(result)
  }
}

// 主进程
import server from './server'
// 类似 koa-router 的使用方式
server.use('test', async (ctx, data) => {
  // data => { a: 1 }  from Renderer Process
  const result = await doSomething(data)
  ctx.reply(['1', '2', '3'])
})



上面代码以 http 请求的写法来处理了 IPC 通信。其中 requestserver

这样使用 async/await 的方式来处理,是不是就轻松多了?代码的可读性、可维护性也增强了。

而且在封装的过程中完全可以按照 axios

废话不多说下面来看一下处理原理。




electron axios 返回的是字符串 electron 调用c_子进程


Electron IPC 异步封装

request 大致实现逻辑:


const { ipcRenderer } = require('electron')

const _map = new Map()

ipcRenderer.on('from-server', (event, params) => {
  const cb = _map.get(params.symbol)
  if (typeof cb === 'function') {
    _map.delete(params.symbol)
    cb()
  }
})

export default request (type, data) {
	const _symbol = Date.now() + type
  return new Promise(resolve => {
    _map.set(_symbol, data => {
      resolve(data)
    })

    ipcRenderer.send('from-type', {
			_symbol, type, data
    })
  })
}


server 大致实现逻辑:


import { ipcMain } from 'electron'

const _map = new Map()

ipcMain.on('from-client', (event, params) => {
    const reply = function (data) {
      event.reply('from-server', {
        _symbol: params._symbol,
        // data 传递给客户端,最终 resolve 它
        data
      })
    }
    const ctx = {
      reply,
      type: params.type
    }
    const cb = _map.get(params.type)
    if (typeof cb === 'function') {
      cb(ctx, params.data)
    } else {
      // 没有注册~
    }
})

export default function use (type, callback) {
	_map.set(type, cb)
}


一个简单基础版的 IPC 封装就完成了,在此功能上还可以增加一些 timeout多次调用只获取最后一次返回的数据类似的功能,这些在原生的 electron IPC API 上是无法实现的(也可能是我文档看的少没有发现)。

测试

起初在做单元测试时,走了弯路。

因为 IPC 是两个进程之间交互的一个过程,当时一直在想如何简单的启动一个 electron 容器进行测试。

后来又根据官网介绍想通过 Node 启动两个进程来模拟 IPC 交互过程,这其实也是个弯路。

其实需要做的只要保证 use、request 方法能跑通、保证内部逻辑运行正确即可。

最后直接模拟了 ipcRenderer (on 和 send)、ipcMain (on 和 reply),将这 4 个方法的输入与输出与 electron 提供的 API 表现一致即可达到目的。

类似于 测试驱动 模拟一个测试环境可以让代码正常运行,且表现一致。