背景介绍
Electron
客户端是以前端技术栈为主,由于前端技术的迭代更新速度比较快,所以为了提高和优化应用的性能,我们的应用也需要不断升级。并且业务需求的不断增加,应用也会面临重新发版升级的问题。当我们发版更新比较频繁时,如果升级的方式不够友好、不够优雅,那么对用户的体验就会造成很大影响。 所以要考虑各种情况,选择比较优雅的升级方法。本文主要围绕以electron-builder
打包方式打包应用时,如何实现应用更新而展开,希望对大家能够有所帮助。
应用UI层更新方案
Electron
应用是由主进程和渲染进程组成。主进程主要为web应用提供native能力,而渲染进程负责UI交互。在一些业务场景下主进程代码没有改动, 只是渲染进程代码有更新,因此客户端只需更新渲染进程即可,也就是主进程创建的window
窗口只需要重新加载新的html页面即可,具体流程如下。
更新步骤(如图1-1)
- 客户端主进程通过轮询、定时请求或者服务端推送方式接收更新通知
- 在主进程中对需要更新的渲染进程执行
webContents.reloadIgnoringCache()
完成页面重载
图1-1
应用全量升级解决方案
当应用代码改动较大时,比如Electron
版本升级、项目架构调整等,我们就可能需要用户下载全量的升级包来升级。Electron
官方提供了多种应用更新方案 参考链接,主要包括使用Electron
团队维护的update.electronjs.org
实现自动更新,以及electron-builder
打包方案中的electron-updater
。我们下问介绍的就是使用electron-updater
完成应用的全量升级。
electron-builder
打包配置
在打包配置文件中,需要配置publish
选项,指定provider和url
, 其中url
是存放升级更新包的服务器。
打包配置文件
{
"appId": "com.test.app",
"productName": "APP",
"files": [
"./build/**/*"
],
"compression": "maximum",
"directories": {
"output": "dist"
},
"publish": [
{
"provider": "generic",
"url": "http://static.test.com" >> 资源服务器
}
],
"mac": {
"target": [
"dmg",
"zip"
]
}
}
打包生成的文件
打包完成之后,我们将打包目录下生成的所有文件都放到资源服务器,给应用提供下载。
├── mac
├── app-0.0.2-mac.zip
├── app-0.0.2.dmg
├── app-0.0.2.dmg.blockmap
├── builder-effective-config.yaml
├── latest-mac.yml
latest-mac.yml文件内容(图1-2)
图1-2
更新步骤(如图1-3,1-4)
- 客户端通过定时检测、或者服务端推送方式检测是否有更新
- 执行
autoUpdater.checkForUpdates()
的检测逻辑,读取资源服务器latest-mac.yml
文件,对比文件hash摘要 - 有更新则执行文件下载操作,可以配合UI层显示下载进度
- 下载完毕之后,通知UI层并显示本次更新的相关内容
- 应用重启进行更新
图1-3
图1-4
electron-updater
更新参考代码
import { autoUpdater } from 'electron-updater';
// 接收应用层通知,是否需要进行版本更新检查
ipcMain.on('update-version-now', () => {
// 调用更新检测方法, 会触发下边注册好的事件
autoUpdater.checkForUpdates();
});
// 接收应用层确认更新的通知, 调用自动退出,并且完成更新
ipcMain.on("update-version-confirm", () => {
autoUpdater.quitAndInstall();
app.quit();
});
// 设置更新服务器url
autoUpdater.setFeedURL(feedURL);
// 监听error
autoUpdater.on("error", function(error) {
mainWindow.webContents.send("update-version-error");
});
// 检测开始
autoUpdater.on("checking-for-update", function() {
mainWindow.webContents.send("checking-for-update");
});
// 更新可用
autoUpdater.on("update-available", function(info) {
mainWindow.webContents.send("update-available");
});
// 更新不可用
autoUpdater.on("update-not-available", function(info) {
mainWindow.webContents.send("update-not-available");
});
// 更新下载进度事件
autoUpdater.on("download-progress", function(progress) {
mainWindow.webContents.send("download-progress", progress);
});
// 更新下载完毕
autoUpdater.on('update-downloaded', function () {
//下载完毕,通知应用层 UI
mainWindow.webContents.send('update-version-downloaded');
});
全量更新方案的优缺点
优点
- 打包配置简单,只需添加
publish
即可 - 代码逻辑简单,添加
autoUpdater
逻辑即可
缺点
- 安装包体积过大时,浪费带宽,增加用户升级时间
- 代码改动量小时,全量升级完全没有必要
- 当我们的应用依赖一些第三方SDK时,当第三方SDK有打包问题时,可能会出现升级失败的情况
应用增量更新升级解决方案
应用在代码改动较少的情况下,用户体验好、比较优雅的更新方式就是增量更新了。增量更新的方案也有多种,具体的增量更新方案需要针对具体的业务需求进行定制。下边介绍2种常见的增量更新方案,我们仍然是基于electron-builder
的打包方式来实现的。
方案一: 固定模块升级(使用asar
代码压缩)
asar
是 Electron
提供的一种将多个文件合并成一个文件的类tar
风格的归档格式,不仅可以缓解Windows
下路径名过长的问题, 还能够略微加快一下 require
的速度, 并且可以隐藏你的源代码( 并非绝对隐藏,专业人士还是可以解压缩的
asar
方式下应用的启动流程(如图1-5)
要想完成 asar
方式下应用的更新,我们必须先了解Electron
应用在这种模式下是如何启动。 其实在这种模式下Electron
应用在启动时 会读取app.asar.unpacked
目录中的内容合并到app目录中进行加载,因此增量更新时我们只需要替换应用安装目录中的app.asar.unpacked
目录,然后重启应用即可。
图1-5
基于electron-builder
的 asar
打包配置
了解了asar
方式下应用的启动流程之后,我们就可以对electron-builder
的打包配置文件进行修改,设置成asar
模式, 具体的配置文件如下。
{
"appId": "com.test.app",
"productName": "APP",
"files": [
"./build/**/*"
],
"compression": "maximum",
"asar": true,
"asarUnpack": [
"./build/src", >> 不需要打包到asar中的文件,也就是有改动的代码
"./build/sdk/one",
"./build/sdk/two"
],
"directories": {
"output": "dist"
},
"mac": {
"target": [
"dmg",
"zip"
]
}
}
打包生成文件
根据配置文件打包后会生成安装包和增量包,其中 app.asar
压缩文件就是基本不要变动的代码,app.asar.unpacked
目录就是我们的增量文件,也就是需要变更的代码。 最后注意我们要对增量包进行压缩,减少更新包体积,然后上传文件到服务器。
├── mac
├── Contents
├── _CodeSignature
├── Frameworks
├── MacOS
├── Resources
├── app.asar >> 这就是我们的asar包,也就是不需要改动的代码
├── app.asar.unpacked >> 这就是我们的增量包,修改更新的代码
├── app-0.0.2-mac.zip
├── app-0.0.2.dmg
├── app-0.0.2.dmg.blockmap
├── builder-effective-config.yaml
└── latest-mac.yml
更新过程(如图1-6)
- 客户端通过定时检测、或者服务端推送方式检测是否有更新
- 通过版本比对发现更新,并获取到需要更新的文件名称
- 下载
app.asar.unpacked.xxx.tgz
更新文件到应用的指定目录(路径因系统而异) - 解压覆盖原文件,重启应用
图1-6
应用安装路径示例
MacOS /Applications/App.app/Contents/Resources
Windows D:/xxx/App/resources
更新过程参考代码
//下载更新文件
fetch(downloadURL).then(res => {
let stream = fs.createWriteStream(unpackPath);
res.body.pipe(stream);
stream.on('close', () => {
//执行解压更新操作
uncompressAndUpdate();
});
}).catch(err => {
logger.error(`download ${downloadURL}: ${err.toString()}`);
});
function uncompressAndUpdate () {
//先备份当前的 app.asar.unpacked目录
fs.renameSync(untgzPath, `${untgzPath}.back`);
compressing.tgz.uncompress(unpackPath, appPath).then(res => {
logger.info(`uncompress ${asarName} success`);
deleteDirSync(`${untgzPath}.back`);
//解压之后,重启应用即可
app.relaunch();
app.exit(0);
}).catch(err => {
//记录错误日志
logger.error(`uncompress ${asarName} error: ${err.toString()}`);
fs.renameSync(`${untgzPath}.back`, untgzPath);
});
}
asar
方式的优缺点
优点
- 可以对代码进行压缩,在一定程度上隐藏源码、提高加载速度
-
asar
和asarunpacked
分开, 很方便的实现增量更新
缺点
- 只能在一定程度上隐藏源码,使用 asar 可以方便的解压缩(可靠的方法还是对源码进行混淆、压缩)
- asar压缩文件中存在 Node API 的局限性, 无法实现非压缩下的所用功能,对于有对Node执行有强需求的可能要仔细斟酌该方案, 笔者就遇到了无法执行
child_process.exec,child_process.spawn
方法的问题
方案二: 自定义模块升级(非asar
方式)
使用非 asar
方式,可以让我们的应用拥有更多的灵活性。下边我们介绍的方式,就是充分利用了electron-builder
中两个常用的配置选项extraResources
(拷贝资源到打包目录Resources中)、extraFiles
(拷贝资源到打包目录的根路径), 详细文档 帮助我们轻松实现增量更新。
基于electron-builder
的非asar
打包配置文件
{
"appId": "com.test.app",
"productName": "App",
"files": [
"./build/**/*"
],
"asar": false,
"asarUnpack": [],
"compression": "maximum",
"directories": {
"output": "dist"
},
"mac": {
"target": [
"dmg",
"zip"
],
"icon": "./build/src/icons/app.icns",
"extendInfo": {
"CFBundleURLSchemes": [
"schema"
]
},
"extraResources": [ >> 拷贝需要的资源
{
"from": "./SDK/",
"to": "SDK"
}
]
}
}
根据配置打包,生成安装包
打包完成生成如下的文件。
app
目录是我们的应用目录。 SDK
目录就是我们额外拷贝过去的目录。 SDK
都属于改动较小的部分,而我们的app
是改动比较频繁的目录。因此增量更新其实就是替换app目录即可,而完全不需要重新下载SDK,当然如果SDK也需要更新的话,更新逻辑中可以添加SDK的更新即可。 在打包完成之后,使用压缩脚本自动将 app
目录压缩生成 app_mac_0.0.2.tgz
,它就是我们的更新包,上传到更新服务器。
├── mac
├── Contents
├── _CodeSignature
├── Frameworks
├── MacOS
├── Resources
├── app >> 应用代码
├── SDK >> 应用代码中依赖的SDK,被拷贝过来的
├── app-0.0.2-mac.zip
├── app-0.0.2.dmg
├── app-0.0.2.dmg.blockmap
├── builder-effective-config.yaml
└── latest-mac.yml
非asar
增量更新代码流程(图1-7)
- 客户端定时、轮询,或者服务端push方式通知检测更新
- 从服务器下载需要更新的文件
- 解压并覆盖已有的文件
- 退出重启应用
图1-7
升级参考代码(和以上的方式类似),只列举了app的更新
//下载更新文件
fetch(appURL).then(res => {
let stream = fs.createWriteStream(appTgzPath);
res.body.pipe(stream);
stream.on('close', () => {
uncompressAndUpdate();
});
}).catch(err => {
logger.error(`download ${appURL}: ${err.toString()}`);
});
function uncompressAndUpdate () {
//先备份当前的 app.asar.unpacked目录
fs.renameSync(appUnTgzPath, `${appUnTgzPath}.back`);
compressing.tgz.uncompress(appTgzPath, appPath).then(res => {
logger.info(`uncompress ${appTgzName} success`);
deleteDirSync(`${appUnTgzPath}.back`);
app.relaunch();
app.exit(0);
}).catch(err => {
logger.error(`uncompress ${appTgzName} error: ${err.toString()}`);
fs.renameSync(`${appUnTgzPath}.back`, appUnTgzPath);
});
}
非asar
方式的优缺点
优点
- 不用考虑asar的各种限制
- 更新方式极其灵活,可根据业务方便的进行定制
缺点
- 无法对代码进行归档压缩(其实对源码对混淆压缩才是真的隐藏)
常用组合
由于项目的不断迭代更新,我们的项目会有不同的升级需求, 既可能有全量更新的版本,又可能有增量更新的版本, 所以我们的应用要同时支持这两种方式。经过以上几种方式的分析对比,我们只需要把全量更新的方案和非asar
方案组合到一起即可, 只要添加完善的更新检测,区分执行哪一种即可轻松搞定。
参考electron-builder
配置
{
"appId": "com.test.app",
"productName": "APP",
"files": [
"./build/**/*"
],
"asar": false, >> 配合实现增量更新
"compression": "maximum",
"directories": {
"output": "dist"
},
"publish": [
{
"provider": "generic", >> 完成全量更新
"url": "http://static.test.com"
}
],
"mac": {
"target": [
"dmg",
"zip"
],
"icon": "./build/src/icons/app.icns",
"extendInfo": {
"CFBundleURLSchemes": [
"meishiim"
]
},
"extraResources": [
{
"from": "./SDK/",
"to": "SDK"
}
]
}
}
应用的整体更新架构(图1-8)
图1-8
自定义的更新检测逻辑(图1-9)
- 客户端定时、轮询,或者服务端push方式通知检测更新(包含灰度)
- 根据
package.json
文件获取要更新的文件 - 从服务器上下载所有更新文件
- 解压并覆盖原文件
- 退出重启应用
图1-9
总结
通过对以上几种更新方案的介绍,相信大家对 Electron
应用更新应该有了一个更加直观的认识。每一种方案都有其适用的应用场景,选择哪一种则需要根据具体的业务需求来综合考虑。以上的更新方案都是笔者根据业务需求探索总结的,如果有其他方案,可以共同探讨~