electron 应用开发优秀实践
在团队中,我们因业务发展,需要用到桌面端技术,如离线可用、调用桌面系统能力。什么是桌面端开发?一句话概括就是:以 Windows 、macOS 和 Linux 为操作系统的软件开发。对此我们做了详细的技术调研,桌面端的开发方式主要有 Native 、 QT 、 Flutter 、 NW 、 Electron 、 Tarui 。其各自优劣势如下表格所示。
一、技术背景
Electron 是多进程架构,架构具有以下特点:
由一个主进程和 N 个渲染进程组成
主进程承担主导作用,用于完成各种跨平台和原生交互
渲染进程可以是多个,使用 Web 技术开发,通过浏览器内核渲染页面
主进程和渲染进程通过进程间通信来完成各种功能
这里说下 Electron 进程间通信技术原理:
electron 使用 IPC (interprocess communication) 在进程之间进行通信,如下图所示:
二、应用技术选型
2.1 编程语言 Typescript
2.2 构建工具 Electron-Forge
2.3 Web 方案 Vue3 + Vite
2.4 monorepo方案 pnpm + turbo
2.5 数据库 lowdb
electron 应用数据库有非常多的选择如 lowdb 、 sqlite3 、 electron-store 、 pouchdb 、 dedb 、 rxdb 、 dexie 、 ImmortalDB 等。这些数据库都有一个特性,那就是无服务器。
给出四个最优选择,分别是 lowdb 、 sqlite3 、 nedb 、 electron-store , 理由如下:
- lowdb: 生态、能力、性能三方面表现优秀, json 形式的存储结构, 支持 lodash 、 ramda 等 api 操作,利于备份和调用
- sqlite3: 生态、能力、性能三方面表现优秀, Nodejs 关系型数据库第一选择方案
- nedb: 能力、性能三方面表现优秀,缺点是基本不维护了,但底子还在,尤其操作是 MongoDB 的子集,对于熟悉 MongoDB 的使用者来说是绝佳选择。
- electron-store: 生态表现优秀,轻量级持久化方案,简单易用
三、构建
3.1 应用图标生成
不同尺寸图标的生成有以下方法 Windows
软件生成: icofx3
网页生成: https://tool.520101.com/diannao/ico/(opens new window)
不同尺寸图标的生成有以下方法 MacOS
软件生成: icofx3
网页生成: https://tool.520101.com/diannao/ico/(opens new window)
命令行生成: 使用 sips 和 iconutil 生成
3.2 二进制文件构建
3.3 按需构建
3.4 性能优化
主要是构建速度和构建体积优化,构建速度这块不好优化。本文重点说下构建体积优化,这里拿 mac 系统举例说明, 在 electron 应用打包后,查看应用包内容
可以看出 asar 中的文件,就是我们构建后的项目代码,从图中可以看到有 node_modules 目录, 这是因为在 electron 构建机制中,会自动把 dependencies 的依赖全部打到 asar 中。
- 将 web 端构建所需的依赖全部放到 devDependencies 中,只将在 electron 端需要的依赖放到 dependencies
- 将和生产无关的代码和文件从构建中剔除
- 对跨平台使用的二进制文件,如 ffmpeg 进行按需构建(上文按需构建已介绍)
- 对 node_modules 进行清理精简
这里提下第 4 点,如何对 node_modules 进行清理精简呢?
如果是 yarn 安装的依赖,我们可以在根目录使用下面命令进行精简:
yarn autoclean -I
yarn autoclean -F
如果是 pnpm 安装的依赖,第 4 点应该不起作用了。我在项目中使用 yarn 安装依赖,然后执行上述命令后,发现打包体积减少了 6M , 虽然不多,但也还可以。
四、更新
4.1 全量更新
通过下载最新的包或者 zip 文件,进行软件更新,需要替换所有的文件。
按照流程图去实现,我们需要做以下事情:
- 开发服务端接口,用来返回应用最新版本信息
- 渲染进程使用 axios 等工具请求接口,获取最新版本信息
- 封装更新逻辑,用来对接口返回的版本信息进行综合比较,判断是否更新
- 通过 ipc 通信将更新信息传递给主进程
- 主进程通过 electron-updater 进行全量更新
- 将更新信息通过 ipc 推送给渲染进程
- 渲染进程向用户展示更新信息,若更新成功,则弹出弹窗告诉用户重启应用,完成软件更新
4.2 增量更新
通过拉取最新的渲染层打包文件,覆盖之前的渲染层代码,完成软件更新,此方案只需替换渲染层代码,无需替换所有文件。
- 渲染进程定时通知主进程检测更新
- 主进程检测更新
- 需要更新,则拉取线上最新包
- 删除旧版本包,复制线上最新包,完成增量更新
- 通知渲染进程,提示用户重启应用完成更新
- 全量更新和增量更新各有优势,多数情况下,采用增量更新来提高用户更新体验,同时使用全量更新作为兜底更新方案。
五、性能优化
5.1 启动时优化
- 使用 v8-compile-cache 缓存编译代码
- 优先加载核心功能,非核心功能动态加载
- 使用多进程,多线程技术
- 采用 asar 打包:会加快启动速度
- 增加视觉过渡:loading + 骨架屏
5.1.1 使用 v8-compile-cache 缓存编译代码
[其他使用方法请查看此链接文档 ](https://www.npmjs.com/package/v8-compile-cache(opens new window))
5.1.2 优先加载核心功能,非核心功能动态加载
export function share() {
const kun = require('kun')
kun()
}
5.2 运行时优化
- 对渲染进程 进行 Web 性能优化
- 对主进程进行轻量瘦身
5.2.1 对渲染进程 进行 Web 性能优化
用一个思维导图来完整阐述如何进行 Web 性能优化,如下图所示:
5.2.2 对主进程进行轻量瘦身
核心方案就是将运行时耗时、计算量大的功能交给新开的 node 进程去执行处理。
const { fork } = require('child_process')
let { app } = require('electron')
function createProcess(socketName) {
process = fork(`xxxx/server.js`, [
'--subprocess',
app.getVersion(),
socketName
])
}
const initApp = async () => {
// 其他初始化代码...
let socket = await findSocket()
createProcess(socket)
}
app.on('ready', initApp)
通过以上代码,将耗时、计算量大的功能,放在 server.js ,然后再 fork 到新开 node 进程中进行处理。
六、质量保障
6.1 自动化测试
6.2 崩溃监控
对于 GUI 软件,尤其桌面端软件来说,崩溃率非常重要,因此需要对崩溃进行监控。
崩溃监控技巧
- 渲染进程崩溃后,提示用户重新加载
- 通过 preload 统一初始化崩溃监控
- 主进程、渲染进程通过 process.crash() 进行模拟崩溃
- 对崩溃日志进行收集分析
- 崩溃监控做好后,如果发生崩溃,该如何治理崩溃呢?
6.3 崩溃治理
崩溃治理难点:
- 定位出错栈困难:Native 错误栈,无操作上下文
- 调试门槛高:C++ 、 IIdb/GDB
- 运行环境复杂:机器型号、系统、其他软件
崩溃治理技巧:
- 及时升级 electron
- 用户操作日志和系统信息
- 复现和定位问题比治理重要
- 把问题交给社区解决,社区响应快
- 善于用 devtool 分析和治理内存问题
七、安全
7.1 源码泄漏
目前 electron 在源码安全做的不好,官方只用 asar 做了一下很没用的源码保护,到底有多没用呢?
你只需要下载 asar 工具,然后对 asar 文件进行解压就可以得到里面的源码了,如下图所示:
7.2 asar
asar 是一种将多个文件合并成一个文件的类 tar 风格的归档格式。Electron 可以无需解压整个文件,即可从其中读取任意文件内容。
7.3 源码保护
避免源码泄漏,按照从低到高的源码安全,可以分为以下程度
- asar
- 代码混淆
- WebAssembly
- Language bindings
其中,Language bindings 是最高的源码安全措施,其实使用 C++ 或 Rust 代码来编写 electron 应用代码,通过将 C++ 或 Rust 代码编译成二进制代码后,破译的难度会变高。这里我说下如何使用 Rust 去编写 electron 应用代码。