- 发布于
Electron 进程通信封装
- Authors
- Name
- 田中原
Electron 进程通信封装
前言
此想法是在使用 Electron 进程间通信(IPC)过程中,无法忍受其 API 的使用不友好性而产生。
为了提高代码可读性、可维护性,而不得已造轮子了。
ELectron 中的 IPC 说明
在 Electron 中分为两个进程:
- Main Process(主进程)。是 Node.js 跑的一个进程,可以调用 Node API 和 Electron 封装好的 API。
- 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 通信。其中 request 与 server 则是进行二次封装后的轮子。
这样使用 async/await 的方式来处理,是不是就轻松多了?代码的可读性、可维护性也增强了。
而且在封装的过程中完全可以按照 axios 的 API 参数来处理,便于代码迁移(当然这里业务逻辑的通用性另说)。
废话不多说下面来看一下处理原理。
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 封装就完成了
测试
因为 IPC 是两个进程之间交互的一个过程,当时一直在想如何简单的启动一个 Electron 容器进行测试。
后来又根据官网介绍想通过 Node 启动两个进程来模拟 IPC 交互过程,这其实也是个弯路。
其实需要做的只要保证 use、request 方法能跑通、保证内部逻辑运行正确即可。
最后直接模拟了 ipcRenderer
(on 和 send)、ipcMain
(on 和 reply),将这 4 个方法的输入与输出与 Electron 提供的 API 表现一致即可达到目的。
类似于 测试驱动
模拟一个测试环境可以让代码正常运行,且表现一致。