人人都会AI编程

17.6 更新异常处理与降级方案

更新时间:2026-07-11

自动更新的理想流程是“检测 → 下载 → 安装 → 重启”,但现实网络中任何环节都可能出错:更新服务器宕机、用户网络断连、签名校验失败、磁盘空间不足……如果处理不当,轻则更新失败后版本停滞,重则应用崩溃或出现不可恢复的状态。本节不追求覆盖所有极端场景,而是聚焦于工程中最高频、最致命的几类异常,并给出直接可用的处理逻辑与降级思路。

17.6.1 更新流程中的典型异常分类

首先明确一个前提:不要把异常处理写成“一把抓”的 try-catch,而是按更新阶段分层控制。这样出问题时可以精确定位,也能为每种异常设计不同的降级行为。

  1. 检查更新阶段失败
  • 表现:autoUpdater.checkForUpdates() 抛出错误或触发 error 事件。
  • 原因:更新服务器不可达、URL 配置错误、返回非 200 状态码、证书过期或自签名证书不被信任。
  • 影响:应用无法获知是否有新版本,用户停留在当前版本。
  1. 下载更新阶段失败
  • 表现:download-progress 事件中断,随后触发 error 或长时间无进度。
  • 原因:网络波动、代理配置错误、更新包过大导致超时、磁盘写入权限不足。
  • 影响:已下载的临时文件可能损坏,需清理并重新尝试。
  1. 安装更新阶段失败
  • 表现:update-downloaded 之后调用 quitAndInstall() 但应用重启后仍是旧版本。
  • 原因:Windows 上的安装程序签名校验失败、macOS 的 .dmg 挂载或拷贝时被系统策略阻止、Linux 包管理器的依赖冲突或权限问题。
  • 影响:用户经历了重启但未享受到更新,体验极差。
  1. 更新中断后状态不一致
  • 表现:更新到一半应用被强制关闭或系统崩溃,导致部分文件写入完成、部分未写入。
  • 原因:用户强制退出、系统断电、杀毒软件拦截文件操作。
  • 影响:最严重,可能使应用无法启动,需要强制回退或修复。

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),实时监控自动更新的成功率。通过日志分析,可以快速发现服务器证书过期、新版本包损坏等问题,在用户大面积受影响前修复。

总结:更新异常处理不是一次写完的代码,而是随着用户场景不断完善的容错体系。抓住“分阶段捕获 → 可重试 → 降级可用 → 用户知情”这四个原则,即便更新流程偶尔断裂,应用整体体验依然可控。下一节将介绍多渠道灰度发布策略,帮助你让更新本身的风险降到最低。