Electron 自动更新,绕过 latest.yml 使用自定义接口

1,194 阅读6分钟

基于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,那么写一个接口如下: 该接口就是自定义的检测更新接口。接口返回值中至少要包含 versionpathsha512 三个属性(与 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,

};
    1. 新建文件 serverTest/index.js
// index.js

const { startServer } = require("./server"); 

// 启动服务器

startServer();
    1. 执行 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"}

在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] 取消安装");

}

}

}

参考文档

  • 以上文档非常感谢以下作者提供的帮助。

# 手撸 Electron 自动更新,再繁琐也要搞懂它

Electron 自动更新,绕过 latest.yml 使用自定义接口