应用更新系统
系统架构
更新系统由以下核心组件构成:

Preview
配置项
在 resources/config/config.ini 中配置:
ini
# 是否在应用启动时检查更新
checkForUpdatesOnStart = true
# 更新服务器地址
updateVersionInfoUrl = https://your-update-server.com/
# 版本信息文件名
versionInfoFile = version.json版本信息格式
服务器上的 version.json 文件格式:
json
{
"version": "1.2.0",
"path": "updates/app-1.2.0.zip",
"releaseDate": "2025-12-09",
"releaseNotes": "修复了若干问题,增加了新功能",
"mandatory": false
}字段说明:
version: 最新版本号(遵循 semver 规范)path: 更新包相对路径releaseDate: 发布日期releaseNotes: 更新日志mandatory: 是否强制更新
更新流程
完整流程图
应用启动
↓
检查配置是否启用自动更新
↓
启用?
↓
向服务器请求版本信息
↓
比较版本号
↓
有新版本?
↓
询问用户是否更新
↓
用户确认?
↓
Worker线程后台下载
↓
显示下载进度
↓
下载完成
↓
解压到临时目录
↓
询问用户是否立即重启
↓
立即重启?
↓
重启应用并安装更新
↓
更新完成1. 检查更新
javascript
/**
* 检查更新(但不自动开始下载)
* @param {boolean} manual - 是否为手动检查更新
* @returns {Promise<boolean>} 是否有可用的更新
*/
async checkUpdate(manual = false) {
if (this.updateMode) {
await notifyUpdateStatus('busy', '更新已在进行中...', this.win);
return false;
}
try {
await notifyUpdateStatus('check', '正在检查更新...', this.win);
const latestInfo = await getLatestVersion();
if (!(await this.shouldUpdate(latestInfo))) {
if (manual) {
await notifyUpdateStatus('no-update', '当前已是最新版本', this.win);
}
return false;
}
// 发现新版本,询问用户是否更新
const shouldDownload = await notifyUpdateStatus(
'update',
`当前版本: ${app.getVersion()}, 新版本: ${latestInfo.version}`,
this.win,
{ latestInfo }
);
// 用户确认,开始更新流程
if (shouldDownload) this.startUpdate(latestInfo);
return true;
} catch (error) {
logger.error('检查更新失败:', error);
await notifyUpdateStatus('error', `检查更新失败: ${error.message}`, this.win);
return false;
}
}触发时机:
- 应用启动时(如果
checkForUpdatesOnStart = true) - 用户手动点击"检查更新"菜单
2. 下载更新
使用 Worker 线程后台下载,不阻塞主进程:
javascript
async downloadUpdate(latestInfo) {
await fs.ensureDir(this.tempDir);
const filePath = path.join(this.tempDir, latestInfo.path);
return new Promise((resolve, reject) => {
// 启用子线程下载更新文件
const worker = new Worker(path.join(__dirname, '../workers/download-worker.js'));
// 往worker发送消息,开始下载
worker.postMessage({
url: `${getConfigValue('updateVersionInfoUrl')}${latestInfo.path}`,
filePath,
});
// 监听worker消息
worker.on('message', (message) => {
switch (message.type) {
case 'progress':
// 更新进度条和标题
const { progress, speed } = message.data;
this.win.setProgressBar(progress / 100);
notifyUpdateStatus('download', `更新包下载进度: ${progress}%`, this.win, {
progress: progress / 100,
speed: speed,
});
break;
case 'complete':
worker.terminate();
this.win.setProgressBar(-1);
this.win.setTitle(`${APP_TITLE} - 更新包下载完成`);
resolve(message.data);
break;
case 'error':
worker.terminate();
reject(new Error(message.error));
break;
}
});
});
}下载特性:
- ✅ Worker 线程后台下载,不阻塞主进程
- ✅ 实时显示下载进度和速度
- ✅ 任务栏进度条显示
- ✅ 支持取消下载
- ✅ 下载失败自动清理
3. 解压和安装
javascript
async extractAndInstall(filePath, latestInfo) {
const appPath = process.cwd();
const tempExtractPath = path.join(this.tempDir, 'extract');
const pendingRename = path.join(this.tempDir, 'pending_rename.json');
try {
notifyUpdateStatus('unzip', '正在解压...', this.win);
// 解压到临时目录
process.noAsar = true;
const zip = new AdmZip(filePath);
await zip.extractAllToAsync(tempExtractPath, true);
// 创建更新信息文件,供应用下次启动时使用
await fs.writeJson(pendingRename, {
source: tempExtractPath,
destination: appPath,
updateReady: true,
});
notifyUpdateStatus('ready', '更新已准备就绪,重启应用后将完成安装', this.win);
// 询问用户是否立即重启
const shouldRestart = await notifyUpdateStatus('update-restart', '', this.win, {
latestInfo,
});
if (shouldRestart) {
// 重启应用
app.relaunch({ args: process.argv.slice(1).concat(['--relaunch']) });
app.exit(0);
}
} catch (error) {
logger.error('准备更新失败:', error);
notifyUpdateStatus('error', '更新准备失败,请稍后重试', this.win);
// 清理临时文件
await fs.remove(tempExtractPath);
await fs.remove(pendingRename);
throw error;
}
}4. 重启后安装
应用重启后,检查是否有待安装的更新:
javascript
// 在 app.whenReady() 中检查
const pendingRename = path.join(TEMP_DIR, 'pending_rename.json');
if (await fs.pathExists(pendingRename)) {
const updateInfo = await fs.readJson(pendingRename);
if (updateInfo.updateReady) {
// 执行文件替换
await fs.copy(updateInfo.source, updateInfo.destination, {
overwrite: true,
});
// 清理临时文件
await fs.remove(updateInfo.source);
await fs.remove(pendingRename);
logger.info('更新安装完成');
}
}版本比较
使用 semver 规范比较版本号:
javascript
/**
* 比较两个版本号
* @param {string} v1 - 当前版本
* @param {string} v2 - 目标版本
* @returns {number} -1: v1<v2, 0: v1==v2, 1: v1>v2
*/
function compareVersions(v1, v2) {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const part1 = parts1[i] || 0;
const part2 = parts2[i] || 0;
if (part1 < part2) return -1;
if (part1 > part2) return 1;
}
return 0;
}
/**
* 判断是否需要更新
* @returns {Promise<boolean>}
*/
async shouldUpdate(latestInfo) {
if (!latestInfo.version) return false;
return compareVersions(app.getVersion(), latestInfo.version) < 0;
}版本号格式: major.minor.patch
1.0.0<1.0.1(patch 升级)1.0.1<1.1.0(minor 升级)1.1.0<2.0.0(major 升级)
用户交互
更新状态通知
通过 IPC 通信向渲染进程发送更新状态:
javascript
/**
* 通知更新状态
* @param {string} status - 状态类型
* @param {string} message - 状态消息
* @param {BrowserWindow} win - 窗口实例
* @param {Object} data - 附加数据
*/
async function notifyUpdateStatus(status, message, win, data = {}) {
if (win && !win.isDestroyed()) {
return new Promise((resolve) => {
win.webContents.send('update-status', { status, message, data });
// 需要用户确认的状态
if (status === 'update' || status === 'update-restart') {
ipcMain.once('update-response', (event, response) => {
resolve(response);
});
} else {
resolve(true);
}
});
}
}状态类型
| 状态 | 说明 | 用户操作 |
|---|---|---|
check | 正在检查更新 | 无需操作 |
no-update | 已是最新版本 | 无需操作 |
update | 发现新版本 | 确认是否下载 |
download | 正在下载 | 查看进度 |
unzip | 正在解压 | 等待完成 |
ready | 更新准备就绪 | 无需操作 |
update-restart | 询问是否重启 | 确认是否重启 |
error | 更新失败 | 查看错误信息 |
busy | 正在更新中 | 等待完成 |
手动更新
托盘菜单触发
javascript
// packages/main/src/config/menu-config.js
{
label: '检查更新',
click: () => {
updateHandler.checkUpdate(true); // manual = true
}
}渲染进程触发
javascript
// 渲染进程中
window.electronAPI.checkForUpdates();
// 监听更新状态
window.electronAPI.onUpdateStatus((status, message, data) => {
console.log('更新状态:', status, message);
if (status === 'update') {
// 显示更新对话框
const confirmed = confirm(`发现新版本 ${data.latestInfo.version},是否更新?`);
window.electronAPI.sendUpdateResponse(confirmed);
}
if (status === 'update-restart') {
// 询问是否重启
const confirmed = confirm('更新已准备就绪,是否立即重启?');
window.electronAPI.sendUpdateResponse(confirmed);
}
});更新包制作
1. 打包应用
bash
# 构建渲染进程
pnpm build:renderer
# 打包应用(不创建安装程序,只打包文件)
pnpm electron:dir2. 创建更新包
将打包后的文件压缩为 ZIP 格式:
bash
# Windows
cd dist/win-unpacked
7z a -tzip ../app-1.2.0.zip *
# Linux/macOS
cd dist/linux-unpacked
zip -r ../app-1.2.0.zip *3. 上传到服务器
your-update-server.com/
├── version.json # 版本信息文件
└── updates/
├── app-1.0.0.zip
├── app-1.1.0.zip
└── app-1.2.0.zip # 最新版本4. 更新 version.json
json
{
"version": "1.2.0",
"path": "updates/app-1.2.0.zip",
"releaseDate": "2025-12-09",
"releaseNotes": "- 新增功能A\n- 修复Bug B\n- 优化性能",
"mandatory": false
}安全考虑
1. HTTPS 传输
ini
# 使用 HTTPS 确保传输安全
updateVersionInfoUrl = https://your-update-server.com/2. 文件校验
建议在 version.json 中添加文件哈希:
json
{
"version": "1.2.0",
"path": "updates/app-1.2.0.zip",
"sha256": "abc123...", // 添加文件哈希
"size": 52428800 // 文件大小(字节)
}下载后验证文件完整性:
javascript
const crypto = require('crypto');
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath);
stream.on('data', (data) => hash.update(data));
stream.on('end', () => {
const fileHash = hash.digest('hex');
if (fileHash !== expectedHash) {
throw new Error('文件校验失败');
}
});3. 签名验证
生产环境建议对更新包进行代码签名:
javascript
// electron-builder.config.js
module.exports = {
win: {
certificateFile: './cert.pfx',
certificatePassword: process.env.CERT_PASSWORD,
},
};最佳实践
1. 增量更新
对于大型应用,建议实现增量更新:
javascript
// 只下载变化的文件
{
"version": "1.2.0",
"type": "incremental",
"baseVersion": "1.1.0",
"path": "updates/patch-1.1.0-to-1.2.0.zip"
}2. 更新回滚
安装前备份当前版本:
javascript
async backupCurrentVersion() {
const appPath = process.cwd();
const backupPath = path.join(this.backupDir, `backup-${app.getVersion()}`);
await fs.copy(appPath, backupPath, {
filter: (src) => !src.includes('node_modules')
});
return backupPath;
}3. 静默更新
配置静默下载,用户无感知:
ini
# 在后台静默下载,安装时才提示
silentDownload = true4. 分阶段发布
通过服务端控制,逐步推送更新:
json
{
"version": "1.2.0",
"rollout": {
"percentage": 20, // 推送给 20% 用户
"users": ["beta-group"] // 或指定用户组
}
}故障处理
下载失败
- 自动重试机制(最多 3 次)
- 断点续传支持
- 降级到旧版本
安装失败
- 保留备份文件
- 提供回滚功能
- 记录错误日志
版本冲突
javascript
// 检查版本兼容性
if (latestInfo.minRequiredVersion) {
if (compareVersions(app.getVersion(), latestInfo.minRequiredVersion) < 0) {
// 当前版本过旧,必须更新
mandatory = true;
}
}相关文件
packages/main/src/handlers/update-handler.js- 更新处理器packages/main/src/workers/download-worker.js- 下载 Workerpackages/main/src/utils/update-utils.js- 更新工具函数resources/bin/update.bat- Windows 更新脚本