增量更新
插件版本 v4.0.0,需 electron-egg v4
介绍
增量更新优点
1.绕过 macOS 权限,无需购买开发者账号(600 元/每年)。
2.如果上架其它平台应用商店 ,理论上也可以绕过官方的升级方式。
3.减小更新包大小,约 4 ~ 10 倍,减少大量 cdn 资源费用。
4.用户重启即可完成更新,不需要走安装流程。
5.支持更多平台 win32 位、win64 位、windows7
6.支持额外资源目录 extraResources 更新
7.不限制应用个数
使用步骤
安装
# 安装插件
npm i ee-incremental-updater@4.0.0
# 升级 ee-bin
npm i ee-bin@4.1.9 -D
# 升级 ee-core
npm i ee-core@4.1.4
移动文件 仅仅 windows 平台需要
1. 把文件 '项目/node_modules/ee-incremental-updater/updater.exe' 和 '...../updater32.exe'
复制或移动到 '项目/build/extraResources/updater.exe', '项目/build/extraResources/updater32.exe'
添加生成增量资源的命令
注意
- 注意:asarFile 是 app.asar 的路径,不同平台不同架构,这个路径可能不一样,根据实际填写
- 修改 bin.js 配置(项目左边箭头)
// 项目 /cmd/bin.js
module.exports = {
// 添加一个命令对象
/**
* 增量更新命令
* ee-bin updater --platform=
*/
updater: {
windows_32: {
asarFile: "./out/win-ia32-unpacked/resources/app.asar",
extraResources: [
"./build/extraResources/goapp.exe",
"./build/extraResources/read.txt",
"./build/extraResources/hello/**/*",
],
output: {
directory: "./out",
file: "incremental-latest.json",
zip: "app.zip",
},
cleanCache: false,
},
windows_64: {
asarFile: "./out/win-unpacked/resources/app.asar",
extraResources: [
"./build/extraResources/goapp.exe",
"./build/extraResources/read.txt",
"./build/extraResources/hello/**/*",
],
output: {
directory: "./out",
file: "incremental-latest.json",
zip: "app.zip",
},
cleanCache: false,
},
macos_intel: {
asarFile: "./out/mac/项目名.app/Contents/Resources/app.asar",
extraResources: [
"./build/extraResources/goapp",
"./build/extraResources/read.txt",
"./build/extraResources/hello/**/*",
],
output: {
directory: "./out",
file: "incremental-latest.json",
zip: "app.zip",
},
cleanCache: false,
},
macos_apple: {
asarFile: "./out/mac-arm64/项目名.app/Contents/Resources/app.asar",
extraResources: [
"./build/extraResources/goapp.exe",
"./build/extraResources/read.txt",
"./build/extraResources/hello/**/*",
],
output: {
directory: "./out",
file: "incremental-latest.json",
zip: "app.zip",
},
cleanCache: false,
},
linux: {
asarFile: "./out/linux-unpacked/resources/app.asar",
extraResources: [
"./build/extraResources/goapp",
"./build/extraResources/read.txt",
"./build/extraResources/hello/**/*",
],
output: {
directory: "./out",
file: "incremental-latest.json",
zip: "app.zip",
},
cleanCache: false,
},
},
};
参数说明
asarFile 原始文件,(框架构建后生成的,用来生成增量包)
extraResources 资源目录文件,指定更新的文件
额外资源目录说明
extraResources: [
'./build/extraResources/hello/**/*'
],
举例:
"./build/extraResources/hello/a.txt" - 精确匹配 a.txt 文件
"./build/extraResources/hello/*.txt" - 匹配 hello 目录(不包括子目录)中,所有 以 .txt 结尾的文件
"./build/extraResources/hello/**/*.csv" - 匹配 hello 目录(包括子目录)中,所有 以 .csv 结尾的文件
"./build/extraResources/hello/**/*" - 匹配 hello 目录(包括子目录)及下面的所有文件
更多语法:
星号(*) — 匹配除斜杠(路径分隔符)和隐藏文件(名称以 . 开头)之外的所有内容。
双星(**) - 匹配零个或多个目录。
问号(?) - 匹配除斜杠(路径分隔符)以外的任何单个字符。
方括号([seq]) - 匹配方括号序列中的任何字符。
output 增量更新输出的资源
output.directory 输出的目录
output.file 升级信息文件,见底部说明
output.zip 增量包,见底部说明
cleanCache 清理缓存
命令使用
注意
注:在对应操作系统上生成增量文件,相比 v1.x 版本,增加了 platform 参数
platform 指定平台,值:windows_32、windows_64、macos_intel、macos_apple、linux
在执行 npm run build-w 时,会生成全量资源和增量资源。
# 编辑 package.json 中 scripts 属性
# 追加 && ee-bin updater 命令
# 如:原始的 build-w
"build-w": "ee-bin build --cmds=win64",
# 现在
"build-w": "ee-bin build --cmds=win64 && ee-bin updater --platform=windows_64",
# macOS 同理
把增量资源放到你的 cdn 上面
# 示例
# 打开 项目/out 目录:
./out/incremental-latest-xxx.json // 升级信息文件,如希望只升级某个平台,修改它的版本号即可
./out/app-xxx-1.0.0.zip // 增量包
latest.yml 文件
# 放到 CDN 目录:域名换成你自己的
http://kodo.qiniu.com/electron-egg/
# 放置后如下 域名换成你自己的
● http://kodo.qiniu.com/electron-egg/incremental-latest-xxx.json
代码示例
业务层
./electron/service/updater.js
(填写其中的 config)
"use strict";
const { app: electronApp } = require("electron");
const { logger } = require("ee-core/log");
const { getMainWindow, setCloseAndQuit } = require("ee-core/electron");
const { is } = require("ee-core/utils");
const { isPackaged } = require("ee-core/ps");
const { sleep } = require("ee-core/utils/helper");
const IncrUpdaterPlugin = require("ee-incremental-updater");
/**
* Updater(service层为单例)
* @class
*/
class UpdaterService {
load() {
const status = {
error: -1,
available: 1,
noAvailable: 2,
downloading: 3,
downloaded: 4,
};
const config = {
// CDN 目录url 换成你自己的
url: "https://www.xxxxxx.com/upload/electronegg/",
// 如果有特殊平台,可以指定一个固定的 json 文件
// urlFile: 'incremental-latest-xxx.json',
// 密钥 换成你自己的
secret: "T_4F9pA7bYZ6rX0",
// debug: false,
};
// 设置配置
IncrUpdaterPlugin.setConfig(config);
// 监听可用更新事件
IncrUpdaterPlugin.on("update-available", (info) => {
const content = {
status: status.available,
version: info.version,
};
this._sendToWindow(content);
});
// 监听不可用更新事件
IncrUpdaterPlugin.on("update-not-available", () => {
const content = {
status: status.noAvailable,
};
this._sendToWindow(content);
});
// 监听下载进度事件
// state 包含以下属性:
// {
// percent: 0.6121836146128672,
// size: { total: 9359669, transferred: 5729836 }
// }
IncrUpdaterPlugin.on("download-progress", (state) => {
const content = {
status: status.downloading,
percent: String(Math.round(state.percent * 100)),
totalSize: IncrUpdaterPlugin.bytesChange(state.size.total),
transferredSize: IncrUpdaterPlugin.bytesChange(state.size.transferred),
};
this._sendToWindow(content);
});
// 监听下载完成事件
IncrUpdaterPlugin.on("update-downloaded", () => {
const content = {
status: status.downloaded,
desc: "下载完成",
};
this._sendToWindow(content);
// 托盘插件默认会阻止窗口关闭,这里设置允许关闭窗口
setCloseAndQuit(true);
});
// 监听下载错误事件
IncrUpdaterPlugin.on("error", (error) => {
logger.error(error.msg);
const content = {
status: status.error,
error: "更新失败",
};
this._sendToWindow(content);
});
}
/**
* 检查更新
*/
checkUpdate() {
IncrUpdaterPlugin.checkAvailable({ timeout: 4000 });
}
async download() {
IncrUpdaterPlugin.download();
}
async relaunchApp() {
// 打包安装后才调用
if (!isPackaged()) return;
// 安装并重启
// 等待1秒,让日志打印完毕,因为经过测试上面代码大概在100ms内执行完毕,为了保险起见,等待1秒,让日志打印完
IncrUpdaterPlugin.installApp();
await sleep(1000);
if (is.macOS()) {
electronApp.relaunch();
electronApp.quit();
} else {
electronApp.quit();
}
}
/**
* 本地版本
*/
currentVersion() {
const v = electronApp.getVersion();
return v;
}
/**
* 向窗口发消息
*/
_sendToWindow(content = {}) {
const textJson = JSON.stringify(content);
const channel = "custom/app/updater";
const win = getMainWindow();
win.webContents.send(channel, textJson);
}
}
UpdaterService.toString = () => "[class UpdaterService]";
module.exports = {
UpdaterService,
updaterService: new UpdaterService(),
};
控制器
- 控制器层
./electron/controller/updater.js
"use strict";
const { updaterService } = require("../service/updater");
class UpdaterController {
/**
* 获取应用基础信息
*/
appInfo() {
const appInfo = {
currentVersion: "",
};
appInfo.currentVersion = updaterService.currentVersion();
return appInfo;
}
/**
* 检查是否有新版本
*/
checkForUpdater() {
updaterService.checkUpdate();
return;
}
/**
* 下载新版本
*/
downloadApp() {
updaterService.download();
return;
}
/**
* 安装新版本
*/
relaunchApp() {
updaterService.relaunchApp();
return;
}
}
UpdaterController.toString = () => "[class UpdaterController]";
module.exports = UpdaterController;
预加载层
./electron/preload/index.js
/*************************************************
** preload为预加载模块,该文件将会在程序启动时加载 **
*************************************************/
const { updaterService } = require("../service/updater");
function preload() {
// updater
updaterService.load();
}
/**
* 预加载模块入口
*/
module.exports = {
preload,
};
前端代码
<template>
<section id="hero">
<h1 class="tagline">
<span class="accent">Electron-Egg</span>
</h1>
<p class="description">A fast, desktop software development framework</p>
<p class="actions">
<a class="setup" href="https://www.kaka996.com/" target="_blank"
>Get Started</a
>
</p>
</section>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ipcRoute, specialIpcRoute } from "@/api/index";
import { ipc } from "@/utils/ipcRenderer";
const appVersion = ref("");
const versionTips = ref("");
const available = ref(false);
onMounted(() => {
init();
});
function init() {
ipc.removeAllListeners(specialIpcRoute.appUpdater);
ipc.on(specialIpcRoute.appUpdater, (event, result) => {
result = JSON.parse(result);
console.log(result);
const { version, status, percent, error } = result;
console.log("啊哈哈哈哈");
console.log(status);
if (status == 1) {
available.value = true;
versionTips.value = "有可用更新 v" + version;
// 这里 最好用 element-plus 的 message 提示 我就直接下载了
download();
} else if (status == 2) {
versionTips.value = "已经是最新版本";
// 最好来个提示之类的
} else if (status == 3) {
versionTips.value = "已下载 " + percent + "%";
// 最好用element-plus 的 进度条组件
} else if (status == 4) {
versionTips.value = "下载完成";
window.alert("下载完成,重新启动");
confirmOk();
} else {
// 最好用 element-plus 的 message 提示
window.alert(error);
}
});
ipc.invoke(ipcRoute.getAppInfo).then((result) => {
console.log(result);
const { currentVersion } = result;
appVersion.value = currentVersion;
});
// 检查版本
checkForUpdater();
}
function checkForUpdater() {
ipc.invoke(ipcRoute.checkForUpdater, {});
}
function download() {
ipc.invoke(ipcRoute.downloadApp, {});
}
function confirmOk() {
ipc.invoke(ipcRoute.relaunchApp);
}
</script>
<style scoped>
.updatebutton {
width: 100px;
height: 40px;
background-color: #42d392;
color: #fff;
text-align: center;
line-height: 40px;
cursor: pointer;
margin: 0 auto;
}
section {
padding: 42px 32px;
}
#hero {
padding: 150px 32px;
text-align: center;
height: 100%;
}
.tagline {
font-size: 52px;
line-height: 1.25;
font-weight: bold;
letter-spacing: -1.5px;
max-width: 960px;
margin: 0px auto;
}
html:not(.dark) .accent,
.dark .tagline {
background: -webkit-linear-gradient(315deg, #42d392 25%, #647eff);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.description {
max-width: 960px;
line-height: 1.5;
color: var(--vt-c-text-2);
transition: color 0.5s;
font-size: 22px;
margin: 24px auto 40px;
}
.actions a {
font-size: 16px;
display: inline-block;
background-color: var(--vt-c-bg-mute);
padding: 8px 18px;
font-weight: 500;
border-radius: 8px;
transition: background-color 0.5s, color 0.5s;
text-decoration: none;
}
.actions .setup {
color: var(--vt-c-text-code);
background: -webkit-linear-gradient(315deg, #42d392 25%, #647eff);
}
.actions .setup:hover {
background-color: var(--vt-c-gray-light-4);
transition-duration: 0.2s;
}
</style>
前端路由
frontend/src/api/index.js
/**
* 主进程与渲染进程通信频道定义
* Definition of communication channels between main process and rendering process
*/
const ipcApiRoute = {
test: "controller/example/test",
};
// 增量更新文档
const ipcRoute = {
// updater
getAppInfo: "controller/updater/appInfo",
checkForUpdater: "controller/updater/checkForUpdater",
downloadApp: "controller/updater/downloadApp",
relaunchApp: "controller/updater/relaunchApp",
};
/**
* 自定义频道
* 格式:自定义(推荐添加一个前缀)
* custom chennel
*/
const specialIpcRoute = {
appUpdater: "custom/app/updater", // updater channel
};
export { ipcApiRoute, ipcRoute, specialIpcRoute };
其他说明
- 升级文件信息
window 平台:
./out/incremental-latest-windows_32.json
./out/incremental-latest-windows_64.json
macOS inter 芯片:
./out/incremental-latest-macos-intel.json
macOS 苹果芯片:
./out/incremental-latest-macos-apple.json
linux 平台:
./out/incremental-latest-linux.json
内容举例
{
// 版本
"version": "1.0.0",
// 要下载的文件名
"file": "app-macos-intel-1.0.0.zip",
// 大小
"size": 24824079,
// 验证
"sha1": "3125553050eb63c9051088570701f876ab0b72b3",
// 发布日期
"releaseDate": "2024-09-11"
}
- 增量包
windows 平台:
./out/app-windows-32-1.2.0.zip
./out/app-windows-64-1.2.0.zip
macOS inter 芯片
./out/app-macos-intel-1.1.0.zip
macOS 苹果芯片
./out/app-macos-apple-1.1.0.zip
linux 平台:
./out/app-linux-1.1.0.zip
指定平台
使用 git 新建一个或多个平台的分支 windonws 、macos_inter 或 macos_apple;
注意
Tips:多分支是跨平台开发的最简单有效的方式。它可以避免因配置、资源不同而导致的需要编写复杂的脚本。
理论上 windows 64 位 构建的资源可以在 win32、win64 平台运行;macOS intel 芯片构建的资源可以在 macos_intel、macos_apple 平台运行。
问题排查
检查 CDN 上的资源,能否在浏览器中直接下载
检查 CDN 上的资源,json 文件里面的文件名是否和 zip 文件一致
下载无响应
方案一:在 setCoinfig 配置里,打开 debug,查看日志
方案二:在 setCoinfig 配置里,把 url 的协议 改成 http:// 或 https://
方案三:在 ./electron/index.js 的 ready() 函数添加
async ready () {
// 关闭同源策略
electronApp.commandLine.appendSwitch("disable-web-security");
// 不要尝试解析代理服务器
electronApp.commandLine.appendSwitch("auto-detect", "false");
electronApp.commandLine.appendSwitch("no-proxy-server");
}
npm run start (非安装环境)下
,仅到下载步骤,无安装替换效果,因为这时候软件未安装。升级插件后,客户端需要迭代两个版本才能看到插件新功能效果。因为用户已经安装的是旧版本,迭代一次后更新了插件,再一次迭代才能看到 上一次的效果