最近一直在做Electron的项目,随着项目的上线,在此做一点总结。本文主要记录一些坑点,整个Electron的搭建流程有很多文章说的非常好了,就不赘述了。
主要包括
关于主进程和渲染进程通信的痛点
持久化数据的方案选型
打包后文件目录的访问权限
关于主进程和渲染进程通信的痛点
先看一下官方的说明
Electron为主进程( main process)和渲染器进程(renderer processes)通信提供了多种实现方式,如可以使用ipcRenderer 和 ipcMain模块发送消息,使用 remote模块进行RPC方式通信
这里提到了两种通信方式。前者需要通过事件注册发布的方式实现数据传递,后者屏蔽了通信的细节,可以像调用普通对象一样共享数据。
例如获取列表数据,使用事件注册发布的方式代码大概是这样
// 在主进程中
const { ipcMain } = require('electron')
ipcMain.on('getList', (event, arg) => {
//获取数据的代码
//实际中可能类似service.getList(arg)
event.sender.send('getList-done', 'list data');
})
复制代码// 在渲染进程中
const { ipcRenderer } = require('electron')
ipcRenderer.on('getList-done', (event, arg) => {
console.log(arg); // 输出 'list data'
})
ipcRenderer.send('getList', 'list args')
//组件销毁后移除这个监听
ipcRenderer.removeAllListeners('getList-done')
//或者ipcRenderer.removeListener移除指定的监听
复制代码
而使用remote模块,代码大概是这样
// 在主进程中
const { app } = require('electron')
// 封装的一个关于数据库请求的service
app.service = new Service()
复制代码// 在渲染进程中
const {remote} = require('electron')
// 通过remote模块直接调用getList
const res = remote.app.service.getList()
复制代码
通过对比可以看出,使用前者会导致很多的样板代码,而使用remote模块则非常方便。然而看似美好的remote模块在实际使用中暴露了很多问题。getList查询在主进程耗时10ms,通过remote模块拿到数据却需要将近100ms。
这一度令我不能理解,在一步步调试后发现,每次访问对象都会进入metaToValue的方法,包括对象上属性的访问。此外还会造成渲染进程卡顿,查阅文档了解到由于remote的本质是发送同步间进程消息,会阻塞主进程,换言之如果一个查询比较耗时,则用户界面会直接卡死甚至出现未响应的情况。
关于这里的问题,我理解有限,大家有兴趣的可以移步荒山大佬的文章,里面有对remote模块的详细分析。
由于remote模块天生的缺陷,还是只能使用事件注册的形式去进行进程间通信。但是实际项目中需要很频繁的和主进程通信(访问本地数据库),如果为每一个方法都注册事件,实在过于繁琐。下面给出我的一个封装方案:
// 在主进程中
// 先提前实例化好service挂到app对象上
ipcMain.on("queryData", async function (event, args) {
const {
serviceName,
serviceMethod,
serviceArg,
key
} = args;
const res = await app[serviceName][serviceMethod](...serviceArg)
event.sender.send("queryFinish", {
key,
data: res
})
})
复制代码type ipcParam = {
serviceName: string;
serviceMethod: string;
serviceArg: any | null;
};
export function ipcHelper(args: ipcParam) {
//生成一个唯一id
const key = getLongId();
let listener: any = null;
const bindEvent = (r: any) => {
return async (event: any, arg: any) => {
//通过key来唯一确定收发消息的双方
if (arg.key === key) {
r(arg.data);
ipcRenderer.removeListener("queryFinish", listener);
}
};
};
return new Promise((r, j) => {
ipcRenderer.send("queryData", {
...args,
key
});
listener = bindEvent(r);
ipcRenderer.on("queryFinish", listener);
});
}
// 调用
export function getList(...params: any):Promise{
return ipcHelper({
serviceName:'testService',
serviceMethod: "getList",
serviceArg: params
});
}
// 拿到结果
const res = await getList('参数')
...
复制代码
经过对事件的promise化,在渲染进程,也就是实际的页面当中,就可以像调用Ajax请求一样方便地请求本地数据了。
持久化数据的方案选型
由于项目本身的一个重点就是处理本地数据,因此本地数据的持久化存储就成了不得不面对的问题。我们组内当初有两个方案:
indexedDB浏览器内置的数据库,文档型数据库
sqlite成熟的内嵌型数据库,关系型数据库
项目一期决定使用indexedDB,出于以下几点考虑
浏览器内置,无需考虑安装打包可能遇到的问题
完全通过渲染进程,无需和主进程进行通信
文档型数据库,不需要额外学习sql语法,门槛较低
但经过一段时间的测试和开发,发现以下弊端
在数据量超过10w时性能严重下降
多表联合查询受限,往往需要把数据全部加载到内存中操作
调试困难,浏览器内置界面只能够一页页翻看数据,没有找到配套的工具,无法快速查询数据
项目二期用sqlite3替换,有以下优点
性能非常优秀,可以支撑百万数据量级的查询和插入
相关生态丰富,有成熟的第三方工具(我们使用的navicat)可以快速定位问题
关于indexedDB和sqlite3,分别使用了Dexie和Knex来辅助数据的操作。这部分有空的话准备单独写一篇来介绍,这方面的中文资料比较欠缺,项目过程中也踩了不少坑。
打包后文件目录的访问权限
由于使用了sqlite3,因此需要对文件进行生成和访问。在开发模式下很顺利,但打包后遇到了找不到路径的问题。
//使用__dirname作为根路径创建文件在开发环境下没有问题
path.join(__dirname)
复制代码
原因在于electron-builder打包后会把文件都打进了 .asar 包中,__dirname作为js执行的绝对路径\resources\app.asar\dist\electron,
显然位于 .asar包中,参考官网的解释
档案文件中的内容不可更改,所以 Node APIs 里那些会修改文件的方法在使用asar 归档文件时都无法正常工作.
要想解决这个问题,有两个方法
把文件的生成目录指向.asar同级或者更上层的目录,例如path.join(__dirname, '/../..')
package.json中build下设置"extraResources": { //把需要访问的文件移动到外层目录
"from": "template",
"to": "temp"
},
复制代码
总结
大概就这么多,欢迎大家指正和讨论