自动更新的理想流程是“检测 → 下载 → 安装 → 重启”,但现实网络中任何环节都可能出错:更新服务器宕机、用户网络断连、签名校验失败、磁盘空间不足……如果处理不当,轻则更新失败后版本停滞,重则应用崩溃或出现不可恢复的状态。本节不追求覆盖所有极端场景,而是聚焦于工程中最高频、最致命的几类异常,并给出直接可用的处理逻辑与降级思路。
17.6.1 更新流程中的典型异常分类
首先明确一个前提:不要把异常处理写成“一把抓”的 try-catch,而是按更新阶段分层控制。这样出问题时可以精确定位,也能为每种异常设计不同的降级行为。
- 检查更新阶段失败
- 表现:
autoUpdater.checkForUpdates()抛出错误或触发error事件。 - 原因:更新服务器不可达、URL 配置错误、返回非 200 状态码、证书过期或自签名证书不被信任。
- 影响:应用无法获知是否有新版本,用户停留在当前版本。
- 下载更新阶段失败
- 表现:
download-progress事件中断,随后触发error或长时间无进度。 - 原因:网络波动、代理配置错误、更新包过大导致超时、磁盘写入权限不足。
- 影响:已下载的临时文件可能损坏,需清理并重新尝试。
- 安装更新阶段失败
- 表现:
update-downloaded之后调用quitAndInstall()但应用重启后仍是旧版本。 - 原因:Windows 上的安装程序签名校验失败、macOS 的
.dmg挂载或拷贝时被系统策略阻止、Linux 包管理器的依赖冲突或权限问题。 - 影响:用户经历了重启但未享受到更新,体验极差。
- 更新中断后状态不一致
- 表现:更新到一半应用被强制关闭或系统崩溃,导致部分文件写入完成、部分未写入。
- 原因:用户强制退出、系统断电、杀毒软件拦截文件操作。
- 影响:最严重,可能使应用无法启动,需要强制回退或修复。
17.6.2 各阶段的健壮处理实现
以下代码基于 electron-updater(最常用的更新方案),但思想同样适用于其他更新库。
第一步:为更新器绑定全局错误监听
应尽早(一般在主进程启动时)将所有错误事件集中管理,避免某个阶段的异常未被捕获而静默失败。
const { autoUpdater } = require('electron-updater');
const log = require('electron-log'); // 建议使用持久化日志
autoUpdater.on('error', (error) => {
log.error(`更新失败 [${error.code || 'UNKNOWN'}]: ${error.message}`);
// 向渲染进程发送通知,让用户知道当前状态
mainWindow.webContents.send('update-error', {
code: error.code,
message: error.message,
});
});
第二步:检查更新时处理网络与服务端异常
autoUpdater.on('checking-for-update', () => {
mainWindow.webContents.send('update-status', 'checking');
});
autoUpdater.on('update-available', (info) => {
mainWindow.webContents.send('update-status', 'available', info);
});
autoUpdater.on('update-not-available', (info) => {
mainWindow.webContents.send('update-status', 'not-available');
});
// 关键:检测到错误时,如果错误可重试,应提供重试机制
autoUpdater.on('error', (error) => {
if (error && error.code === 'ERR_CONNECTION_REFUSED') {
// 服务端拒绝连接,可能是临时故障,30 秒后自动重试一次
setTimeout(() => {
log.info('重试检查更新...');
autoUpdater.checkForUpdates();
}, 30000);
}
});
第三步:下载阶段的异常与重试
electron-updater 内部自带下载管理,但默认不处理网络中断的重试。可通过监听下载进度并结合手动重试来增强鲁棒性。
let downloadRetryCount = 0;
const MAX_DOWNLOAD_RETRY = 3;
autoUpdater.on('download-progress', (progressObj) => {
mainWindow.webContents.send('download-progress', progressObj.percent);
});
autoUpdater.on('error', (error) => {
if (error && error.message.includes('sha512') || error.message.includes('checksum')) {
// 校验失败,通常是下载不完整,删除缓存并尝试重新下载
log.warn('下载文件校验失败,尝试重新下载...');
if (downloadRetryCount < MAX_DOWNLOAD_RETRY) {
downloadRetryCount++;
// 清理可能的缓存文件(electron-updater 通常自动处理,但手动清理更保险)
const cachePath = getUpdateCachePath();
if (cachePath && fs.existsSync(cachePath)) {
fs.rmSync(cachePath, { recursive: true, force: true });
}
autoUpdater.downloadUpdate();
} else {
log.error('下载更新失败次数过多,放弃更新');
mainWindow.webContents.send('update-error', { message: '下载失败,请检查网络后重试' });
}
}
});
第四步:安装阶段的失败处理
update-downloaded 事件触发后,通常调用 autoUpdater.quitAndInstall() 来安装。但如果安装过程中被系统阻止,应用重启后仍为旧版本。这种情况难以在代码中直接“修复”,但可以设计降级提示:
autoUpdater.on('update-downloaded', (info) => {
log.info('更新包下载完毕:', info.version);
mainWindow.webContents.send('update-status', 'downloaded', info);
// 提示用户重启并立即安装
dialog.showMessageBox({
type: 'info',
title: '更新就绪',
message: `新版本 ${info.version} 已下载,是否立即重启安装?`,
buttons: ['立即重启', '稍后再说'],
defaultId: 0,
}).then(({ response }) => {
if (response === 0) {
// 安装前关闭所有可能占用文件的进程(如数据库连接、子进程等)
cleanupBeforeQuit();
autoUpdater.quitAndInstall();
}
});
});
// 应用重启后,检测版本是否真的更新成功
app.on('ready', () => {
const currentVersion = app.getVersion();
const lastUpdateAttempt = app.getPath('userData') + '/lastUpdateAttempt.json';
try {
if (fs.existsSync(lastUpdateAttempt)) {
const attempt = JSON.parse(fs.readFileSync(lastUpdateAttempt, 'utf-8'));
if (attempt.version !== currentVersion) {
log.warn('上次更新未生效,可能安装失败');
mainWindow.webContents.send('update-failed-install', attempt.version);
}
fs.unlinkSync(lastUpdateAttempt); // 清理记录
}
} catch (e) {
// 忽略
}
});
在发起更新前,可以写入尝试版本信息:
function attemptUpdate(version) {
fs.writeFileSync(
app.getPath('userData') + '/lastUpdateAttempt.json',
JSON.stringify({ version, timestamp: Date.now() })
);
}
17.6.3 完整的降级方案设计
当自动更新彻底失败或用户需要回退到旧版本时,降级机制能避免应用不可用。根据应用的分发方式,降级策略可以分为三类:
方案一:引导式手动降级(最普遍)
适用于通过官网提供固定版本下载的应用。当检测到更新失败且应用可能已损坏时,提示用户前往官网下载旧版本,并提供直链:
// 渲染进程收到安装失败通知后
ipcRenderer.on('update-failed-install', (event, version) => {
showDialog({
title: '更新未生效',
message: `新版本 ${version} 安装失败,建议还原到稳定版本。`,
detail: '您可下载稳定版安装包进行覆盖安装,数据不会丢失。',
buttons: ['前往下载页', '忽略'],
}).then(({ response }) => {
if (response === 0) shell.openExternal('https://yourapp.com/stable-download');
});
});
方案二:内嵌回退版本(适用于企业环境)
在应用目录下预先保留上一个稳定版本的安装包(仅 Windows 可行)。当检测到启动失败次数超出阈值时,自动执行旧版安装程序。
// 主进程启动时检测崩溃计数
const crashCount = app.getLoginItemSettings().crashCount || 0;
if (crashCount > 3) {
const backupInstaller = path.join(process.resourcesPath, 'backup-installer.exe');
if (fs.existsSync(backupInstaller)) {
// 执行旧版安装并退出
require('child_process').exec(`"${backupInstaller}" /SILENT`);
app.quit();
}
}
这种方法维护成本高,通常只在关键生产工具中使用。
方案三:多版本并存与动态切换
高级方案,不作为通用推荐。实现方式类似 Chrome 的“版本目录”,即启动器读取配置决定实际运行的 Electron 版本。维护复杂度较高,只在极大型项目(如 IDE、设计工具)中采用。
17.6.4 更新监控与用户沟通
技术手段之外,坦诚的用户沟通能大幅降低挫败感。更新过程中应提供清晰的界面状态:
- “正在检查更新…” 时显示 loading 动画
- “下载中 45%” 配合进度条
- 下载出错时给出“重试”按钮,并附带可能的解决建议(例如“请检查代理设置”)
- 安装失败后不静默忽略,而是明确告知并给出降级入口
同时,在主进程中建议接入错误上报(如 Sentry),实时监控自动更新的成功率。通过日志分析,可以快速发现服务器证书过期、新版本包损坏等问题,在用户大面积受影响前修复。
总结:更新异常处理不是一次写完的代码,而是随着用户场景不断完善的容错体系。抓住“分阶段捕获 → 可重试 → 降级可用 → 用户知情”这四个原则,即便更新流程偶尔断裂,应用整体体验依然可控。下一节将介绍多渠道灰度发布策略,帮助你让更新本身的风险降到最低。