Electron自动更新实战:清单文件与自建服务器全解析
如果你用 Electron 做过桌面应用,大概率体会过这种感觉:本地开发一切正常,打包也顺利,但一接自动更新,electron-updater 就开始“灵异事件”——一直提示已经是最新版本、下载到一半失败、macOS 更新完打不开。我前前后后帮团队接过三套 Electron 应用的更新链路,每次排查到最后,根子都落在同一个点上:对 latest.yml 和 app-update.yml 这两个文件的理解不够透彻。这篇文章就围绕 electron-builder 的打包产物、清单文件规则和自建更新服务器,把自动更新这条闭环从头到尾拆一遍。
这两年 Electron 技术栈动静挺大,从跨端适配到新系统移植都有不少讨论,但不管应用怎么打包分发,自动更新始终是桌面端上线绕不开的一环。文章基于 electron-builder 25.x 和 electron-updater 6.x 的实践,覆盖 Windows、macOS、Linux 三个平台。适合正在接自动更新、或者准备放弃第三方分发渠道、自己搭更新服务器的桌面端开发同学参考。
1. 自动更新链路拆解:三个文件决定一条更新路径
1.1 桌面端更新的本质:文件交换协议
Web 端改一行代码,用户刷新页面就是最新版。桌面端做不到,你必须经历四步:检查、下载、校验、替换。Electron 的自动更新本质就是把“如何完成这四步”的约定固化下来,而 electron-updater 这个库做的事情,就是按照约定替你把网络请求和文件替换跑完。
理解 electron-updater,关键是搞清楚它依赖哪几个文件。打包时 electron-builder 会产出两类清单:一类是 latest.yml 这种要传到服务器上的远端清单;一类是 app-update.yml 这种打进应用安装目录的本地寻址文件。很多新手只盯着其中一个看,结果排查问题时就像盲人摸象。
1.2 app-update.yml 与 latest.yml 的分工
这两个文件名字相近,但职责完全不同。我直接用一张表说清楚:
| 维度 | app-update.yml(本地寻址) | latest*.yml(远端清单) |
|---|---|---|
| 生成时机 | electron-builder 打包时自动生成 | electron-builder 打包时自动生成 |
| 存放位置 | 应用安装目录 resources 下 | 自建更新服务器的发布目录 |
| 核心内容 | provider、url、channel、updaterCacheDirName | version、path、sha512、files |
| 作用 | 告诉 electron-updater 去哪检查更新 | 告诉 electron-updater 最新版本是什么、去哪下载 |
app-update.yml 里的 url 指向服务器地址,channel 决定请求的清单文件名。客户端启动后,electron-updater 先读这个文件,拼出完整的清单地址,然后去服务器拉取 latest.yml。拉回来的内容里包含了最新版本号、安装包文件名、sha512 校验值,electron-updater 据此决定要不要下载、下载完怎么校验。
1.3 一次完整更新请求的触发过程
举个具体例子。假设本机装的是 1.0.0,服务器上已经发布了 1.1.0,整个过程是这样的:
- 应用启动,electron-updater 读取安装目录里的
app-update.yml,拿到更新服务器地址和 channel。 - 拼接出清单 URL,类似
https://updates.example.com/win/latest.yml,发起请求。 - 拿到
latest.yml后,解析出version: 1.1.0,与本地 1.0.0 做 semver 比较,发现新版存在。 - 按
path指向的文件名,生成安装包下载地址,开始下载。 - 下载完成后,用
sha512字段对文件做校验,确认没有损坏或被篡改。 - 调用平台对应的安装器完成替换。Windows 静默跑 NSIS,macOS 替换 .app 目录,Linux 替换 AppImage 文件。
这套流程里,最容易被忽略的是第 1 步的“寻址”。很多人改完服务器上的文件,发现客户端毫无反应,就是因为 app-update.yml 是打包时写死的,它记录的 URL 还是旧地址。记住一个结论:线上已安装版本里的 app-update.yml 不会因为你改了代码或服务器而自动更新,它只随新包重新安装而更新。
2. electron-builder 打包配置:每一行都不白写
2.1 publish 配置的两种形态
electron-builder 里控制自动更新相关行为的,主要是 publish 配置。它既可以在 package.json 里写,也可以在 electron-builder.yml 里写。我习惯放在独立的 yml 文件里,因为配置项多了以后,package.json 会变得很难读。
这段配置的意思是:使用 generic 类型(也就是通用静态文件服务器),更新地址指向 https://updates.example.com/win,channel 为 latest。打包后,这些信息会被写进 app-update.yml。如果你在 package.json 里写,效果是一样的,但注意 electron-builder 同时读两个来源时,优先级偶有坑,别两个地方都配,二选一最稳。
channel 这个字段值得多说一句。generic provider 请求清单时,文件名不是随手定的,而是由 channel 推导的。Windows 默认请求 latest.yml,macOS 默认请求 latest-mac.yml,Linux 默认请求 latest-linux.yml。channel 的作用,本质上就是让同一个 URL 下可以通过不同通道名称分发不同版本。
2.2 平台差异:为什么 mac 更新要 zip,Windows 要 NSIS
很多第一次接自动更新的人会在 target 配置上翻车。macOS 的正式分发通常用 dmg,但你如果只打 dmg,electron-updater 在 mac 上是没法更新的。macOS 更新要求必须有 zip 格式,dmg 是给用户首次安装用的,更新走的是 zip 包。
我常用的 target 配置是这样的:
Windows 这边,NSIS 是 electron-updater 支持最完整的安装格式,支持差分更新,体验最顺。Linux 下 AppImage 是最常见的选择,但更新完需要重启应用才能生效,这个限制要提前跟用户讲清楚。
2.3 artifactName 的命名与坑
安装包文件名不是随便起的。electron-updater 下载时会把 latest.yml 里的 path 拼到 URL 后面,如果文件名里带了空格,URL 编码处理不好就会 404。更严重的情况是多个平台产物重名,导致服务器上文件互相覆盖。
我的 artifactName 固定写法:
${name} 取自 package.json 的 name,${version} 是版本号。这样生成的文件名唯一且可读,发布到服务器后一眼就知道是哪个版本。命名规范这件事,看起来不起眼,真出了“覆盖旧包导致更新失败”的问题时,你会回来感谢这一节的。
3. latest.yml 字段拆解:版本比较、文件校验与增量更新
3.1 字段逐个看
打包完成后,electron-builder 会在输出目录生成 latest.yml。Windows 下长这样:
逐个解释:
version:最新版本号,必须符合 semver 规范,比如 1.1.0。electron-updater 拿它和本地版本比较。files:文件列表。一般只有一个主安装包,如果开了差分更新,url 和 sha512 对应的就是差分文件。path:安装包相对路径,和 files 里的 url 通常一致。sha512:安装包完整哈希的 base64 编码。注意不是 hex 字符串,是 base64。手动改版本号时如果不同步改 sha512,下载完校验必然失败。releaseDate:发布时间,格式是 ISO 8601。
这里有一个常见的误解:有人试图手动编辑 latest.yml 里的版本号来实现更新。可以这样做,但必须连 sha512 一起改,而且 sha512 要从新包重新计算。与其手工折腾,不如用 npx electron-builder --publish never 重新生成清单文件,让工具去做。
3.2 electron-updater 的版本比较机制
electron-updater 的版本比较不是简单的字符串比较,而是遵循 semver 规则。也就是说,1.10.0 会被正确判断为比 1.9.0 新,不会因为字符串排序而错乱。这看起来很基础,但实际遇到过不少团队因为版本号不规范,比如忘了写第三位,导致更新永远不触发。
另一个容易踩的点是降级保护。electron-updater 默认不会下载低于当前版本的包。也就是说,你发布 1.2.0 之后发现问题,想用 latest.yml 指回 1.1.0 让用户“回滚”,是做不到的。客户端会认为 1.2.0 已经是最新。这是刻意的安全设计,防止把线上稳定版本拉回有问题的旧版本。真要回滚,正确做法是发布一个版本号高于 1.2.0 但代码内容回退到旧逻辑的修复包。
3.3 差分更新与 blockmap 的取舍
Windows NSIS 安装包支持差分更新。打包时 electron-builder 会额外生成一个 .exe.blockmap 文件,latest.yml 里的 files 条目会包含对应信息。electron-updater 会先下载 blockmap,对比本地已安装版本和新版本之间的差异,只下载差异部分,再拼接出完整安装包。实测小版本迭代时,差分包体积通常只有全量包的 10% 到 30%。
差分更新的缺点是计算和拼接过程对网络稳定性有要求,如果下载到一半断流,可能整个拼接失败,然后自动回退到全量下载。electron-updater 会做这个回退,但你会在日志里看到多一次的下载开销。所以:
- 更新频率高、用户量大:建议开启差分,省带宽。
- 更新频率低、内部小团队:直接全量更省事,少一个 blockmap 的排查点。
差分更新默认开启,如果不想用,可以在 electron-builder 配置里关闭相应的生成逻辑,但这并不常见。
4. app-update.yml:被打进应用内部的寻址线索
4.1 生成时机和位置
app-update.yml 是 electron-builder 打包时自动生成的,不需要手动创建。它随安装包一起分发,安装后位于应用的 resources 目录下。Windows 上一般在安装目录的 resources 文件夹里,macOS 则在 .app/Contents/Resources 下。检查已安装应用时,可以用这个命令确认文件是否正常:
如果安装后找不到这个文件,大概率是打包配置出了问题,自动更新绝对跑不起来,先解决这个再排查别的。
4.2 一份典型 app-update.yml 的内容与解释
实际内容因平台而异,Windows 下大致长这样:
provider 和 url 是核心,provider 决定 electron-updater 用哪种更新协议。generic 就是直接访问静态文件服务器;如果配成 github,它就会走 GitHub Releases 的 API。updaterCacheDirName 决定了更新缓存的目录名,这个字段在排查下载失败问题时很有用。
4.3 开发调试更新时的常见玩法
调试自动更新最难受的是,打包一次、安装一次、验证一次,周期太长。我通常的做法是本地起一个静态文件服务器,把 publish 的 URL 临时指向 localhost:
打包安装后,electron-updater 会从本地服务器拉取清单。配合 autoUpdater.setFeedURL 动态覆盖 URL,也可以实现类似效果。但注意,macOS 对非 HTTPS 的更新源限制很严,本地调试 mac 更新时,建议用 http 配合 debug 模式,或者起一个带自签证书的 HTTPS 服务,否则会卡在请求阶段。
4.4 “改了服务器地址不生效”的踩坑
这是被问得最多的问题之一。现象是:代码里把 publish 的 url 改成了新服务器,重新打包也打了,但线上用户永远在请求旧地址。
原因前面讲过:app-update.yml 是打包时生成的,它记录的是打包那一刻的配置。已经安装在用户机器上的版本,不会因为你后来改代码而改变。要让存量用户切换到新服务器,有两个办法:
- 发布一个新版本,让用户在旧地址上还能拉到这个新包,再带着新 url 覆盖安装。
- 如果旧地址完全不可用,只能让用户手动下载新安装包安装,没有捷径。
所以,更新服务器域名和地址,一定要在用户量还小的时候定下来,切换成本会随着用户数增长而急剧上升。
5. 自建更新服务器:目录结构、HTTPS 与灰度放量
5.1 为什么自建服务器只需要静态文件
electron-updater 的 generic provider 是最简单的更新源类型:它只需要一个能下载文件的静态服务器,不需要任何后端逻辑,不用数据库,不用接口。这也意味着,Nginx、OSS、甚至一个简单的 Python http.server 都能当更新源。
和 GitHub Releases 相比,自建服务器最大的优势是没有平台限制和网络限制。GitHub 在国内的访问稳定性、私有仓库的下载鉴权、单文件 2GB 大小限制,都会在某个时间点变成团队的痛点。自建一台小带宽的 VPS 做文件服务,成本不高,但自由度很高。
5.2 推荐的发布目录结构
目录结构决定了后期维护时会不会手忙脚乱。按平台分目录,每个目录放各自的清单和安装包,是我目前最推荐的结构:
发布流程很简单:
- 本地执行 electron-builder 打包,产物复制到对应平台目录的根目录。
- 清清单文件覆盖到同目录。
- 旧版本安装包手动移入 archive,如果要回滚,把旧包从 archive 复制回来,同时恢复旧版清单。
注意 mac 目录下的清单文件名是 latest-mac.yml 而不是 latest.yml,Linux 是 latest-linux.yml。Windows 才是 latest.yml。这个差异来自 electron-builder 多平台构建的设计,目的就是避免多个平台共享服务器目录时同名文件互相覆盖。
5.3 Nginx 配置示例与流量注意
自建更新服务器的核心就是 Nginx 静态服务,配置很轻量:
大文件下载建议保留 Range 支持,Nginx 静态文件默认支持断点续传,electron-updater 下载中断后也会用 Range 续传,所以不要额外关掉。latest.yml 必须走 no-cache,否则升级后客户端可能缓存旧清单,一直拉不到新版本。这个细节我在实际部署中吃过亏,必须重点注意。
5.4 HTTPS 不是选项而是门槛
自建更新服务器必须上 HTTPS,这没有商量余地。原因有两层:
第一,macOS 的更新流程对非 HTTPS 源极其严格,特别是新版本 macOS 对未加密的更新源基本是直接拒绝。Windows 上虽然不会卡得那么死,但如果是白域名或者新域名,走 HTTP 流量很容易被安全软件标记。
第二,自动更新本质上是让用户执行你服务器上的安装包。如果更新通道是明文的,中间人可以把安装包替换成恶意程序。虽然 latest.yml 里有 sha512 校验,但中间人连 latest.yml 本身都可以改。HTTPS 是保障整条链路可信的底线。
证书直接用 Let’s Encrypt 免费证书,三个月自动续签一次,成本为零。
5.5 简单灰度:同一目录、两套清单
自建服务器做灰度其实非常简单,原理就是利用 channel 机制。打包的时候给一个发布通道,写成 beta:
对应的客户端在打包安装后,会请求 https://updates.example.com/win/beta.yml。
生产环境的稳定版客户端请求 latest.yml,测试用户请求 beta.yml。两个清单指向同一个安装包或不同安装包都行。想全量放量时,把 beta.yml 的内容同步到 latest.yml 即可。
整个过程不需要任何后端代码,只需要在发布目录里多放一份 yml 文件。这是自建服务器对比第三方托管服务最实在的优势之一。
6. 上线后实测遇到的三类问题与排查链路
6.1 症状:应用一直提示“已是最新版本”
这是出现频率最高的问题。先说排查顺序,这是我在团队内部框定的标准链路:
- 打开已安装应用,确认
app-update.yml存在,且url、channel正确。 - 用 curl 请求服务端清单,确认服务器上确实是最新版本且文件可访问。
- 对比清单里的
version和本地package.json里的 version,确认远端版本号高于本地。 - 确认你测试的是安装包,而不是用
electron .直接跑源码。开发模式不会走到真实的 app-update.yml。 - 检查服务端
latest.yml有没有被缓存。如果用了 CDN 或 Nginx 代理,清理缓存再看一遍。
实际案例里,我遇到过最隐蔽的一次是 Nginx 对 latest.yml 的 location 匹配没生效,因为前面的 location / 块兜底返回了带缓存头的响应。客户端拿到旧的清单文件,永远认为本地已经是最新。修法是显式声明清单文件的 Cache-Control,也就是 5.3 节里的配置。
6.2 症状:macOS 更新后提示应用已损坏
macOS 更新后的“已损坏”提示,核心原因基本都集中在签名和公证上。自动更新下载的是 zip 包,更新时替换整个 .app。如果 zip 里的 .app 没有正确签名,或者签名机和构建机不一致,更新后 Gatekeeper 就会拒绝运行。
排查命令是这样:
如果 codesign 输出里没有权威的 Team ID,说明签名没配好。macOS 的 electron-builder 配置里,需要确保 build 机配置了证书,同时开启 notarization(公证):
如果没有企业开发者账号,建议在 GitHub Actions 的 macOS runner 上统一构建,把证书导出到 runner 环境变量里,这样签名链路最稳定。本地手工签名再上传,很容易因为钥匙串证书状态不一致而翻车。
6.3 症状:Windows 更新后被杀毒软件拦截
Windows 上最常见的问题是 SmartScreen 和其它安全软件拦截。原因不复杂:未签名的 exe、首次出现在互联网上的新文件指纹,都容易触发云查杀。
这个问题的根本解法只有一条:代码签名证书。Windows 的代码签名证书是花钱的,但如果你面向的是普通用户,这笔钱省不了。签名后,安装包的数字签名会给用户一个明确的信任来源,也极大降低安全软件的误报率。
另外提一个技巧:发布新版本时不要急着全量放量,可以先让一小部分测试用户装新包,观察安全软件拦截率。很多 AV 厂商在新文件首次出现时判定会比较激进,几天后没有恶意行为,信誉分就会恢复。灰度发布在 Windows 更新场景里不光是功能测试手段,也是安全风控手段。
6.4 症状:更新下载到一半失败
下载中断、校验失败、反复更新不成功,这一类问题多半出在三个方面。
第一,服务器带宽或 ACL 限制导致 Range 请求失效。有些云存储开启了 CDN 但关闭了分片下载,electron-updater 的断点续传就会异常。可以在 Nginx 里保留 Range 支持,同时观察访问日志里有没有 206 状态码。
第二,本地缓存目录损坏。electron-updater 会把下载的安装包缓存到 updaterCacheDirName 指定的目录。Windows 下通常在 %LOCALAPPDATA%\myapp-updater,macOS 在 ~/Library/Caches/myapp-updater。如果之前的下载残留了损坏文件,后续更新会反复校验失败。处理方式是先删除缓存目录,再重新检查更新。
第三,磁盘空间不足。NSIS 安装包和 macOS zip 包都需要空间解压。应用自己占用加上安装包体积翻倍,小分区机器很容易不够。这个可以在更新前检查剩余空间,不够时提示用户清理。
我的建议是在主进程里显式监听 error 事件,把错误信息透出到界面上,而不是让用户面对一个静默失败的黑盒:
把错误暴露出来,用户反馈问题时,你也能从错误信息里快速定位到是下载阶段还是安装阶段出了岔子。
7. 发布前记得做一次清单核对
最后分享一个我现在每次发版都会执行的动作。工具自动生成的清单文件虽然可靠,但发布链路里往往存在人工操作,比如手动上传、手动覆盖文件。这些环节最容易出错。
我的核对清单是这样的:
- [ ] 确认 CI 构建生成的 latest.yml / latest-mac.yml / latest-linux.yml 与安装包在同一个目录。
- [ ] 用命令对比本地打包输出目录和服务器目录里的 sha512 是否一致。
- [ ] 确认服务器清单文件的版本号高于上一个已发布版本。
- [ ] 在一台干净环境的测试机上安装全新包,确认 app-update.yml 里的 URL 正确。
- [ ] 触发一次检查更新,观察是否存在 HTTP 缓存导致旧清单的问题。
多平台发布时,建议把各平台的清单文件名和产物列成一张表,贴在 CI 配置文件旁边。团队里每个人发布前都过一遍,比出了问题再回滚舒服得多。自动更新这条路,搭起来不难,真正难的是后续每个版本迭代时不出岔子。希望这篇实操指南能帮你把这条链路从一开始就理清楚。