基于electron-egg基础项目的demo分支,期望使用electron-updater实现自动更新功能,但获取最新版本的部分不使用默认的# latest.yml,而是使用自定义接口的方式。 本文使用自定义接口的方式,绕过了 latest.yml 配置文件,使自动更新的灵活性更高,也更符合我们的常规习惯,但是需要维护一个更新接口(可以使用java,也可以使用js,本文使用的是js搭建的测试更细接口)。
Electron 自动更新的全流程
Electron 的自动更新不会像 React Native 一样直接下载 Web 代码静默更新,因为它还有主进程(Node.js)代码,因此需要走安装流程。
在 Electron 中,使用第三方包 electron-updater
来实现自动更新的功能。
相比 官网的autoUpdater,第三方包 electron-updater
有以下优势:
- 不需要搭建专门的更新服务(如 Hazel、Nuts 等)。
- 同时支持 macOS 和 Windows 签名。
- 支持获取下载进度,等等。
最主要的优势还是支持自定义更新服务。公司发布新版本安装包,上传到自己的静态服务器最方便,而不是还要再搭建一个专门的更新服务。
electron-egg基础项目
创建项目我是基于这个基础项目electron-egg,在这个项目DEMO分支的基础上做完善。
如何本地测试更新?
在开发阶段想测试一下检测更新的流程走没走通,可能不太好测试,因为 Electron 默认在开发环境下会绕过更新检测。
开发环境下 Electron 启动后,如果接入了自动更新,主进程控制台会打印下面的信息:
Skip checkForUpdates because application is not packed and dev update config is not forced
意思是当前是开发环境,未打包,所以绕过检测。Electron 通过 app.isPackaged
的值来判断是否打包,那么在开发环境下,我们可以修改一下这个值:
- 值得注意的是,项目中其他地方在使用isPackaged,随意不能强制更改这个isPackaged。 我采用了另一种方案,使用autoUpdater.forceDevUpdateConfig = true;
if (!electronApp.isPackaged) {
// Object.defineProperty(electronApp, "isPackaged", {
// get: () => true,
// });
autoUpdater.forceDevUpdateConfig = true; // 强制使用开发环境的配置文件,期望代替更改isPackaged,以免影响其他业务
autoUpdater.updateConfigPath = path.join(__dirname, "dev-update.yml");
}
- 重新运行,大概率会看到第二个错误:
Error: ENOENT: no such file or directory /xxxx/app-update.yml 在根目录下创建一个
dev-update.yml
文件(文件名可自定义),写入配置:
provider: generic
updaterCacheDirName: ee-updater # 下载目录
然后在开发环境指定这个配置文件地址:
autoUpdater.updateConfigPath = path.join(__dirname, "dev-update.yml");
重新运行项目,会发现DEBUG下检测更新的逻辑可以正常执行了。
能不能绕开 latest.yml,走后端接口?
我希望的检查更新流程是这样:
调用后端的 API 接口,接口返回 JSON 格式数据,包含最新的版本号和安装包下载地址。将该版本号与本地版本号做对比,如果不一样则表示有更新,并执行下载。
然而 electron-updater
是通过 latest.yml
文件来获取版本号等信息。
latest.yml 是一个配置文件,内容如下:
version: 1.0.2
files:
- url: elapp_1.0.2.exe
sha512: xxxxxx
size: 72716511
path: elapp_1.0.2.exe
sha512: xxxxxx
releaseDate: '2023-11-29T02:28:28.032Z'
假设 latest.yml 的访问路由是 /ee/latest.yml
,那么写一个接口如下:
该接口就是自定义的检测更新接口。接口返回值中至少要包含 version
,path
,sha512
三个属性(与 latest.yml 中的配置保持一致)。这样我们不需要上传 latest.yml 文件了,用该接口替代即可。
基于该检测更新接口,接下来我们逐步实现自定义更新流程。
自定义服务器接口的具体步骤
- 1 新建文件夹 serverTest。
- 2 新建文件 serverTest/package.json
{
"dependencies": {
"express": "^4.19.2"
}
}
- 3 新建文件 serverTest/server.js
// server.js
const express = require("express");
const app = express();
const port = 3000;
// 封装成一个方法
function startServer() {
// 定义一个 GET 路由
app.get("/", (req, res) => {
res.send("Hello, World!");
});
app.get("/ee/:platform", (req, res, next) => {
let { platform } = req.params;
let { elearch } = req.headers;
console.log(`originalUrl = ${req.originalUrl}`);
console.log(`query = ${JSON.stringify(req.query)}`);
console.log(`platform = ${platform}`);
console.log(`elearch = ${elearch}`);
let resinfo = null;
// 返回 Windows 配置
if (platform == "latest.yml") {
resinfo = {
code: 0,
msg: "成功",
version: "1.0.2",
path: "xxx_1.0.2.exe",
sha512: "xxxxxx",
};
}
// 返回 Mac 配置
if (platform == "latest-mac.yml") {
// resinfo = {
// code: 0,
// msg: "成功" ,
// version: "3.10.0",
// // path:
// // "https://yunwu-public-dev.oss-cn-hangzhou.aliyuncs.com/install/ee/ee-mac-3.10.0-arm64.dmg",
// // url: "ee-mac-3.10.0-arm64.dmg",
// // sha512:
// // "rgEvpCdElKm5s3KXp2XjwSVkwwZ6Pjv39u+iDN3653lBSFdEPCQsDmy9OA9cyyBo+wSGzy5QQ216KzgkuQeL2w==",
// // size: 120177439,
// path:'https://yunwu-public-dev.oss-cn-hangzhou.aliyuncs.com/install/ee/ee-mac-3.10.0-arm64.zip',
// url: 'ee-mac-3.10.0-arm64.zip',
// sha512: 'j6gDGkMSSV0hX5QYPz+gKWu//mVO/I/PKHJFqNKF0X1YcqnbCW2+TUmAAAzV3xg47fDpw14QU7f6yxcm2zm9kw==',
// size: 115203182,
// content:'本次更新了 1 个功能,优化了 1 个功能。'
// };
// 没有版本更新时 和有版本更新时 官方返回的结果都一样,都是以下的格式。
resinfo = {
files: [
{
sha512:
"vFlUCW+SdhNvyvY42TIVYwn8FWkkMd9pxvbrovZ0mZREJk+dinrNo13VhbqV7drMu3suGg7LOSZ7XAwdK633dw==",
size: 121529651,
url: "ee-mac-3.10.0-arm64.zip",
},
{
sha512:
"9DmPOoprCHFGydS2/3Z/mWE2bGm1Ys0ZVAmpM9hyfLaZfqUTkaOmv0REPlh30wRY4S8EB2KJ/3ROl2va5kCY+w==",
size: 125163208,
url: "ee-mac-3.10.0-arm64.dmg",
},
],
path: "ee-mac-3.10.0-arm64.zip",
releaseDate: "2024-05-07T10:46:31.141Z",
sha512:
"vFlUCW+SdhNvyvY42TIVYwn8FWkkMd9pxvbrovZ0mZREJk+dinrNo13VhbqV7drMu3suGg7LOSZ7XAwdK633dw==",
version: "3.10.0",
};
}
if (!resinfo) {
resinfo = {
code: 400,
msg: "参数错误",
version: "",
path: "",
sha512: "",
};
}
res.send(resinfo);
});
// 启动服务器
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
}
// 导出方法
module.exports = {
startServer,
};
-
- 新建文件 serverTest/index.js
// index.js
const { startServer } = require("./server");
// 启动服务器
startServer();
-
- 执行 npm install
- 6 启动服务 node index.js
- 7 验证服务是否正常
http://localhost:3000/ 返回 Hello, World!
http://localhost:3000/ee/latest-mac.yml 返回
{"files":[{"sha512":"vFlUCW+SdhNvyvY42TIVYwn8FWkkMd9pxvbrovZ0mZREJk+dinrNo13VhbqV7drMu3suGg7LOSZ7XAwdK633dw==","size":121529651,"url":"ywxd-mac-3.10.0-arm64.zip"},{"sha512":"9DmPOoprCHFGydS2/3Z/mWE2bGm1Ys0ZVAmpM9hyfLaZfqUTkaOmv0REPlh30wRY4S8EB2KJ/3ROl2va5kCY+w==","size":125163208,"url":"ywxd-mac-3.10.0-arm64.dmg"}],"path":"ywxd-mac-3.10.0-arm64.zip","releaseDate":"2024-05-07T10:46:31.141Z","sha512":"vFlUCW+SdhNvyvY42TIVYwn8FWkkMd9pxvbrovZ0mZREJk+dinrNo13VhbqV7drMu3suGg7LOSZ7XAwdK633dw==","version":"3.10.0"}
-
- 对于 Windows 和 Mac 两个系统的更新,electron-updater 使用不同的配置文件,分别是
latest.yml
和latest-mac.yml
。 请求路径分别是 http://localhost:3000/ee/latest-mac.yml 和 http://localhost:3000/ee/latest.yml 。
- 对于 Windows 和 Mac 两个系统的更新,electron-updater 使用不同的配置文件,分别是
- 9 如何添加自定义参数? 可以在路径后面拼接,比如: http://localhost:3000/ee/latest-mac.yml?platform=mac&&arch=arm64
在electron-update中设置刚刚搭建好的后台服务地址。
import { autoUpdater } from 'electron-updater';
// 设置检测更新的地址
this.httpUrl = 'http://localhost:3000/ee';
// 拼接请求参数
const params = {
osType: os.type(),
osPlatform: os.platform(),
...
};
const paramsQuery = Object.keys(params)
.map((key) => `${key}=${params[key]}`)
.join("&");
const FeedURL = `${this.httpUrl}/update?${paramsQuery}`;
// 设置检测更新的地址 ,入参形如:http://localhost:3000/ee?osType=Windows_NT&osPlatform=x64&productName=X3AIClient&softwareVersion=1.0.0
autoUpdater.setFeedURL(FeedURL);
// 不自动下载
autoUpdater.autoDownload = false;
// 触发检测
autoUpdater.checkForUpdatesAndNotify().catch();
// 监听到可更新
autoUpdater.on('update-available', (info) => {
// info 是检测更新接口返回的数据
if (info.can_download) {
// can_download 是自定义属性
autoUpdater.downloadUpdate();
}
});
通过这种方式,即便我们更新了安装包,也可以自由决定是否要下载安装。
- autoUpdater.on('update-available',返回结果如下:
{
"version": "1.0.2",
"path": "http://xxx/xxx_1.0.2.exe",
"sha512": "xxxxxx",
"can_download": false
}
这里有一个小惊喜:path
属性的值可以是一个完整的安装包地址,这样可以把安装包上传到任意地方。如果值是一个文件名,那么会以第(2)步中 setFeedURL()
方法设置的地址为前缀。
提醒:sha512 属性的值必须从打包生成的 latest.yml 中获取,不可以随意写,否则在安装时不能通过检验,会报这个错:
Error: sha512 checksum mismatch
若后台接口没有查到任何版本信息,怎么办?
autoUpdater.on("error", (err) => {
let info = {
status: status.error,
desc: err,
};
Log.error(`[addon:autoUpdater] current desc: ${info.desc}`);
// Error: This file could not be downloaded, or the latest version (from update server) does not have a valid semver version:
if (
err.message.includes("This file could not be downloaded") ||
err.message.includes("does not have a valid semver version")
) {
info.desc = "当前版本已是最新版本,无需更新";
info.status = status.noAvailable;
Log.error(
`[addon:autoUpdater] 此错误表示当前版本已是最新版本,无需更新 `
);
}
this.sendStatusToWindow(info);
});
如何让用户决定是否现在下载?
- 是否强制更新?若是,提示正在下载。
- 若不是强制更新,则由用户决定是否下载更新。
/**
* 下载更新
*/
download(info) {
if (this.cfg.force === "true") {
// 静默下载
autoUpdater.downloadUpdate(); // 自动下载更新
} else {
const res = dialog.showMessageBoxSync({
type: "info",
title: "升级提示",
message: "有新版本可用,是否立即下载新版本?",
detail: `${info.content}`,
cancelId: 1, // 用于取消对话框的按钮的索引
defaultId: 0, // 设置默认选中的按钮
buttons: ["确认", "取消"], // 按钮及索引
});
let data = res === 0 ? "点击确认按钮" : "点击取消按钮";
Log.info(`[addon:autoUpdater] 点击按钮: ", ${data}`);
if (res === 0) {
Log.info("[addon:autoUpdater] 启动下载...");
autoUpdater.downloadUpdate(); // 自动下载更新
} else {
Log.info("[addon:autoUpdater] 取消下载");
}
}
}
如何让用户决定现在是否立即安装?
- 判断是否强制更新,若是,弹窗提示后直接启动退出安装流程。
- 若不是强制更新,弹窗提示是否立即安装。
installNow(info) {
if (this.cfg.force === "true") {
const res = dialog.showMessageBoxSync({
type: "info",
title: "升级提示",
message: "正在安装新版本,请耐心等待...",
detail: `${info.content}`,
// cancelId: 1, // 用于取消对话框的按钮的索引
// defaultId: 0, // 设置默认选中的按钮
// buttons: ["确认", "取消"], // 按钮及索引
});
autoUpdater.quitAndInstall(true, true);
setTimeout(() => {
const { CoreApp } = EE;
CoreApp.appQuit();
}, 5000);
} else {
const res = dialog.showMessageBoxSync({
type: "info",
title: "升级提示",
message: "新版本已经下载完成,是否立即安装?",
detail: `${info.content}`,
cancelId: 1, // 用于取消对话框的按钮的索引
defaultId: 0, // 设置默认选中的按钮
buttons: ["确认", "取消"], // 按钮及索引
});
let data = res === 0 ? "点击确认按钮" : "点击取消按钮";
Log.info(`[addon:autoUpdater] 点击按钮: ", ${data}`);
if (res === 0) {
Log.info("[addon:autoUpdater] 启动安装...");
// isSilent, isForceRunAfter , 静默更新,更新完后立即运行
autoUpdater.quitAndInstall(true, true);
setTimeout(() => {
const { CoreApp } = EE;
CoreApp.appQuit();
}, 5000);
} else {
Log.info("[addon:autoUpdater] 取消安装");
}
}
}
参考文档
- 以上文档非常感谢以下作者提供的帮助。