知行札记
专题技术实践桌面应用实践Electron 桌面应用

自动更新与版本切换

区分更新路线、产物、元数据、下载、安装和失败恢复。

本页整合 Electron 归档分稿中的机制、接口和接线。归档使用 Electron 44 系列作为其版本讨论背景;本文接口依据官方文档及 breaking changes 核查,实际采用时仍需固定项目版本。包版本、维护状态和原稿实测均是对应日期的历史信息。本次未启动 Electron、构建安装包、签名、公证或验证更新。代码块按各自标注区分片段与组合示例。

6.1 两条技术路线

Electron 生态存在两条自动更新路线:内置 autoUpdater(main process 模块,基于 Squirrel 系框架)与 electron-updater(electron-builder 生态的独立 npm 包)。官方文档对内置模块的定位是「目前仅支持 macOS 和 Windows」,Linux 明确建议交给发行版包管理器;而 electron-updater 三平台可用,是 2026 年社区事实标准。

维度内置 autoUpdater(Squirrel)electron-updater(electron-builder)
支持平台macOS、WindowsmacOS、Windows、Linux(AppImage/DEB/RPM/Pacman)
Windows 更新目标Squirrel.Windows、MSIXNSIS(Squirrel.Windows 已不支持,官方建议迁移 NSIS)
feed 元数据macOS 自定义 JSON / Windows RELEASES 文件latest.yml / latest-mac.yml / latest-linux.yml
差量更新Squirrel.Windows 的 nupkg deltablockmap(NSIS 安装器、macOS zip、AppImage)
staged rollouts无原生支持,需服务端分流stagingPercentage(客户端判定)
下载进度事件无download-progress
签名校验macOS 签名校验(Squirrel.Mac 要求)macOS + Windows Authenticode + sha512 校验(v27 起支持 Ed25519 manifest 签名)
配置方式代码里 setFeedURL构建期生成 app-update.yml 内嵌于 resources
适配工具链Electron Forge / electron-winstallerelectron-builder

6.2 全链路原理与流程

两种方案的链路一致:启动或定时触发 checkForUpdates() → 拉取 feed 元数据 → 版本比较 → 下载(优先差量)→ 完整性/签名校验 → 安装(下次启动生效或 quitAndInstall())。

6.2.1 内置 autoUpdater(Squirrel 系)

  • 检查:macOS 上 Squirrel.Mac 请求 feed URL;有更新时服务器返回 200 + JSON(url 字段必需,指向更新 zip;name/notes/pub_date 可选),无更新返回 204 No Content。Windows Squirrel.Windows 则在 feed 的 /RELEASES 子路径返回固定格式文件(SHA1、.nupkg 文件名、大小),且即使无更新也要返回响应——版本比较由客户端完成。setFeedURL 还支持 serverType(json/default,仅 macOS)与 file:// 协议(可绕过鉴权困难的服务器)。
  • 下载与安装:update-available 后自动下载;update-downloaded 触发后即使不调用 quitAndInstall(),更新也会在下次启动时生效;quitAndInstall() 会先关闭所有窗口再退出并安装(此时应监听 before-quit-for-update 做清理,而非 before-quit)。
  • Windows 双机制:Electron 自动探测——传统安装器走 Squirrel.Windows;以 MSIX 打包(process.windowsStore)时走 MSIX 机制,支持直链 .msix 或 JSON feed,并可用 allowAnyVersion 允许降级。
  • 托管服务:官方免费服务 update.electronjs.org(条件:公开 GitHub 仓库 + 发布到 GitHub Releases + macOS 代码签名),配套 update-electron-app 一行接入,启动时检查且每 10 分钟轮询;也支持静态对象存储(serverless)模式——macOS 读 releases.json(currentRelease + releases[],Forge 的 maker-zip 配 macUpdateManifestBaseUrl),Windows 读 RELEASES(maker-squirrel 配 remoteReleases)。

6.2.2 electron-updater(electron-builder)

流程与内置版相同,但元数据改为 latest.yml 系列:构建时生成并上传(除 generic provider 需手动上传外),客户端读取内嵌的 app-update.yml(通常不要调用 setFeedURL)。关键选项及默认值(以官方源码为准):autoDownload = true、autoInstallOnAppQuit = true(v26;v27 起由 autoInstallEvent 取代:"onQuit"(默认)/"onNextLaunch"/"manual")、allowDowngrade = false(仅在 channel 不同、即 semver pre-release 段不同才生效)、allowPrerelease(仅 GitHub provider,默认跟随当前版本是否含 pre-release 段)。

channel 机制:channel 决定客户端读取哪份元数据文件——latest(默认)对应 latest.yml,beta 对应 latest-beta.yml(macOS/Linux 为 latest-mac.yml/latest-linux.yml 的 beta 变体)。detectUpdateChannel 可按当前版本号的 pre-release 段自动推断 channel(如 0.12.1-alpha.1 归入 alpha,GitHub provider 除外);generateUpdatesFilesForAllChannels 则让一次发布同时产出多个 channel 的元数据文件,用于把 beta 用户逐步「扶正」到 stable。

平台实现差异值得注意:

  • macOS(MacUpdater):更新包必须是 zip 工件(找不到则抛 ERR_UPDATER_ZIP_FILE_NOT_FOUND);下载完成后通过本地代理服务器 + Basic auth 把文件交给 Squirrel.Mac,Squirrel 原生 staged——「退出后下次启动时应用更新」,quitAndInstall 在 macOS 实际是委托原生 updater。已下载的 zip 会缓存为 update.zip 供下次差量。
  • Windows(NsisUpdater):quitAndInstall 通过 spawn 安装器完成,参数为 --updated,静默时加 /S,isForceRunAfter 对应 --force-run,自定义目录 /D= 必须是最后一个参数;遇到 EACCES 时经 elevate.exe 重试提权。签名校验见 6.3。
  • Linux(AppImageUpdater):仅在 APPIMAGE 环境变量存在时启用(Snap 环境直接禁用);下载新 AppImage → 校验通过后才 chmod 0o755 → 删除旧文件并 mv 替换运行中的 AppImage → 可选重启(或以 APPIMAGE_EXIT_AFTER_INSTALL 退出)。findFile 显式排除 rpm/deb/pacman 工件——deb/rpm 没有应用内自更新,需依赖 apt/dnf 或商店渠道;AppImage「仅通过 manifest checksum 验证」。

6.2.3 macOS:签名、公证与 Sparkle 替代

  • 签名是硬性要求:官方文档原话「Your application must be signed for automatic updates on macOS. This is a requirement of Squirrel.Mac.」;electron-builder 文档同样强调「macOS application must be signed in order for auto updating to work」。
  • 公证(notarization):官方更新文档未将公证列为更新链路要求(只提签名);社区教程普遍要求「signed and notarized」(公证是 macOS 上通过 Gatekeeper 分发的通行要求,建议开启)。更换 Developer ID 证书后老版本客户端能否继续更新的具体行为,官方文档未说明,待核实。
  • Sparkle 替代:原生 Mac 应用的事实标准更新框架 Sparkle(Sparkle 2,macOS 10.13+)提供 EdDSA 签名、delta 更新与沙盒支持。Electron 官方无 Sparkle 桥,社区项目(如 Innei/electron-sparkle-updater 的 N-API 桥 + appcast 工具链)声称可绕开「Squirrel.Mac 需要付费 Developer ID、Sparkle 可用 ad-hoc 签名」的痛点——此说法来自第三方 README,待核实,采用前需自行验证。

6.2.4 Windows:Squirrel.Windows vs NSIS

Squirrel.Windows 已事实上冻结:最后 release 2.0.1(2020-09-27),仓库 README 公开招募维护者,423 个 open issue;electron-builder 已不再支持它作为更新目标,官方迁移建议是 NSIS。Squirrel.Windows 遗留的两个经典问题(官方文档记载):安装后首启带 --squirrel-firstrun 参数且 Squirrel 持有文件锁导致检查失败——应跳过该次检查或延迟约 10 秒(electron#7155);以及必须调用 app.setAppUserModelId(格式如 com.squirrel.slack.Slack)否则任务栏固定异常。NSIS + electron-updater 是 2026 年 Windows 默认选型,天然支持 channel(latest/latest-beta.yml 等)与 blockmap 差量。

6.3 差量更新(blockmap)与下载校验

blockmap 原理:electron-builder 构建时为可更新工件(NSIS 安装器、macOS zip、AppImage)生成 .blockmap(文件分块的哈希索引)。更新时客户端取新旧两份 blockmap,只下载发生变化的分块;旧 blockmap 无缓存、或 manifest 指定 blockMapUrl 而无 override 时,以及任何异常情况下都会自动回退全量下载;disableDifferentialDownload 可关闭,previousBlockmapBaseUrlOverride 可指定旧 blockmap 地址(GitHub 场景常用)。electron-builder 26.17.0 还新增了 NSIS store-asar 差量模式。Squirrel.Windows 的差量则走自己的 nupkg delta,与 blockmap 无关。

校验链(层层递进):

  1. latest.yml 中的 sha512 对下载文件做哈希校验,失败抛 ERR_CHECKSUM_MISMATCH;
  2. Windows 上用 PowerShell Get-AuthenticodeSignature 校验安装器 Authenticode 签名,并与 app-update.yml 中 publisherName 做 Subject CN 匹配,失败抛 ERR_UPDATER_INVALID_SIGNATURE;v26 中 publisherName 缺失时跳过校验仅告警(fail-open),v28 起将改为失败(fail-closed);缓存的安装器在下次启动安装前会重新验签;
  3. v27(alpha)引入 Ed25519 签名的更新 manifest(CI 经 ELECTRON_BUILDER_UPDATE_SIGN_KEY 配置,支持双签实现 key rotation),客户端配置公钥后 fail-closed;同时 SHA-256-only 元数据已弃用,v28 将强制 SHA-512。

安全视角:Doyensec 2026-02 研究指出主流方案未覆盖四类威胁:降级/回滚攻击(MITM 或被入侵的服务器下发旧版本)、完整性攻击(篡改二进制/元数据)、TOCTOU 竞态(校验与安装之间被替换本地文件)、未测试版本攻击(alpha/beta 与生产共用签名 key/通道);该机构 2020 年就披露过 electron-updater 签名校验绕过导致 RCE 的案例。其参考实现 SafeUpdater 采用 Ed25519 签名 + SHA-512 + 「签名内容绑定版本号」来封死降级路径——这预示 electron-builder v27 的 Ed25519 manifest 正是对同类威胁的响应。

6.4 更新策略:静默 vs 征询、灰度、回滚与强制最低版本

  • 静默 vs 征询:autoDownload=true + 下次启动生效是最省打扰的组合;征询式则把 autoDownload=false,在 UI 中展示版本说明后手动 downloadUpdate()/quitAndInstall()。v27 的 autoInstallEvent: "onNextLaunch" 会在下次启动时对照最新 update info 重新校验缓存安装器再安装,规避了系统会话中途关机导致的安装损坏(历史 issue #7807)。
  • 灰度(staged rollouts):在 latest.yml/latest-mac.yml 中手写 stagingPercentage: 10 即向 10% 用户放量,可随时间调大。客户端判定算法(源码实证):首次生成 UUID v5 随机 ID 持久化于 userData 下 .updaterId,UUID.parse(id).readUInt32BE(12) / 0xffffffff 得到 [0,1) 值,小于 stagingPercentage/100 则命中;字段缺失视为全量;由于 .updaterId 持久不变而判定值恒定,阈值从 10 逐步调到 50、100 的过程就是同一批用户被单调放量,不会来回抖动;检查请求会携带 x-user-staging-id 头,服务端可据此自行分流。大型应用实践:VS Code 自 1.107(2025-11)起将 Insiders 构建在 4 小时窗口、Stable 在 24 小时窗口内渐进放量,用户可用 Check for Updates 立即获取。
  • 回滚:官方文档明确——没有降级通道。要撤回已灰度的坏版本,必须发布一个更高版本号;重新发布同号版本只会让已中招的用户停在坏版本上。allowDowngrade 只解决「beta 用户回流 stable」这类跨 channel 场景。
  • 强制最低版本:内置 autoUpdater 与 electron-updater 均没有内建 min-version/kill-switch API。通行的两种做法:一是服务端按客户端版本分流(自定义 feed 服务天然能做,内置 Squirrel feed 本就由服务器决定返回 200 还是 204);二是客户端启动时额外拉取一份远程配置(最低版本/维护开关)强制升级或禁用。
  • 检查频率:update.electronjs.org 的默认是启动 + 每 10 分钟;官方教程的 60 秒示例偏激进,生产建议数小时级并加随机抖动,同时注意官方警告「调用 checkForUpdates() 两次会把更新下载两遍」。

6.5 Feed 服务选型对比

方案类型活跃度(2026-10 经 GitHub API 核实)要点
update.electronjs.org官方托管Electron 团队维护免费免运维;要求公开 GitHub 仓库 + Releases,macOS 必须签名
GitHub Releases provider托管electron-builder 维护私有仓库需 GH_TOKEN,限额 5000 请求/小时、每次检查约 3 个请求,官方称仅适合「特殊场景」
S3/OSS/Spaces/R2 + generic静态托管自管纯静态 + CDN,成本最低;latest.yml 必须配 no-cache/短缓存,否则永远查不到新版本
electron-release-server自建最后提交 2024-04,半停滞带管理面板与鉴权,不依赖 GitHub
Nucleus(Atlassian)自建最后提交 2023-12,release 停在 2018(v0.8.3)多应用、多 channel 的元数据服务
Nuts(GitBook)自建最后提交 2023-10GitHub Releases 后端 + 磁盘缓存,支持私有仓库
faynoSync 等新一代项目自建活跃(2026-10 仍在提交)支持 Electron/Tauri 多生态的轻量更新服务,社区规模小

结论:官方教程仍列出的 Hazel/Nuts/electron-release-server/Nucleus 多数已失修,2026 年的主流是「GitHub Releases 或对象存储 + generic provider」与大厂自研更新服务(如 VS Code 的自有 update service)两端;选自建方案前务必核对仓库活跃度。

6.6 常见坑与反模式

  1. 重复调用 checkForUpdates()——官方明示会重复下载;务必做去重/节流。
  2. Squirrel.Windows 首启文件锁(--squirrel-firstrun)与漏设 app.setAppUserModelId。
  3. macOS 未签名就上线更新——Squirrel.Mac 直接拒绝;「本地能跑、上线全挂」多源于此。
  4. Windows 无签名 + v26 默认 fail-open:publisherName 缺失时校验被跳过,等于裸奔;发布签名安装器并在配置中写明 publisherName。
  5. latest.yml 被 CDN 长缓存——灰度与紧急止血全部失效;设置 Cache-Control: no-cache。
  6. 想「回滚」到旧版本——做不到,只能发更高版本号;把「发更高版本」写进发布 runbook。
  7. 给 electron-updater 调 setFeedURL——会覆盖构建期生成的 app-update.yml,除鉴权头等特殊需求外不要碰。
  8. 开发环境测更新:用 dev-app-update.yml + forceDevUpdateConfig=true;Linux 下设 APPIMAGE 指向产物;推荐本地 MinIO 模拟 generic 服务。
  9. 失败排查缺日志:设置 autoUpdater.logger(推荐 electron-log),关注 error 事件;常见错误码:ERR_CHECKSUM_MISMATCH(文件损坏或元数据与上传物不一致)、ERR_UPDATER_INVALID_SIGNATURE(签名/证书主体不匹配)、ERR_UPDATER_ZIP_FILE_NOT_FOUND(macOS 缺 zip 工件)、差量失败会自动回退全量(日志可见 fallback)。electron-updater 没有内建失败自动重试,依赖下一轮检查。
  10. HTTP feed、把私有 GitHub token 打进客户端、alpha/beta 与生产共用签名通道——均为 Doyensec 点名的攻击面。

6.7 关键代码与配置示例

electron-updater(征询式 + 灰度友好):

const { autoUpdater } = require('electron-updater')
const log = require('electron-log')

function setupUpdater() {
  autoUpdater.logger = log
  autoUpdater.autoDownload = true          // 默认 true
  autoUpdater.autoInstallOnAppQuit = true   // v26;v27 起用 autoInstallEvent

  autoUpdater.on('download-progress', (p) => log.info(`进度 ${p.percent}%`))
  autoUpdater.on('update-downloaded', async ({ version }) => {
    const { response } = await dialog.showMessageBox({
      type: 'info', buttons: ['重启', '稍后'],
      message: `新版本 ${version} 已就绪,重启后生效`
    })
    if (response === 0) autoUpdater.quitAndInstall()
  })
  autoUpdater.on('error', (e) => log.error(e))

  autoUpdater.checkForUpdates()                       // 启动即查
  setInterval(() => autoUpdater.checkForUpdates(), 4 * 60 * 60 * 1000)
}

内置 autoUpdater(官方教程模式,自建 feed 按平台分流):

const { app, autoUpdater } = require('electron')
if (app.isPackaged) {
  const url = `https://updates.example.com/update/${process.platform}/${app.getVersion()}`
  autoUpdater.setFeedURL({ url })
  setInterval(() => autoUpdater.checkForUpdates(), 60 * 60 * 1000)
}

latest.yml 灰度配置(构建后手动追加,逐步调大):

version: 1.1.0
path: MyApp Setup 1.1.0.exe
sha512: Dj51I0q8aPQ3ioaz9LMqGYujAYRbDNblAQbodDRXAMxm...
stagingPercentage: 10   # 先放 10%,观察后调至 50 → 100

electron-builder 发布配置(generic + 对象存储):

publish:
  provider: generic
  url: https://cdn.example.com/myapp/   # 存放 latest.yml / exe / blockmap
  channel: latest                       # beta 渠道 → latest-beta.yml

参考来源

最后更新于

本页目录