Electron自动更新实战:清单文件与自建服务器全解析

electron自动更新electron-builderelectron-updater
于 2026-10-01 04:58:09 修改
·本内容遵循CC 4.0 BY-SA版权协议

如果你用 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,整个过程是这样的:

  1. 应用启动,electron-updater 读取安装目录里的 app-update.yml,拿到更新服务器地址和 channel。
  2. 拼接出清单 URL,类似 https://updates.example.com/win/latest.yml,发起请求。
  3. 拿到 latest.yml 后,解析出 version: 1.1.0,与本地 1.0.0 做 semver 比较,发现新版存在。
  4. 按 path 指向的文件名,生成安装包下载地址,开始下载。
  5. 下载完成后,用 sha512 字段对文件做校验,确认没有损坏或被篡改。
  6. 调用平台对应的安装器完成替换。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 会变得很难读。

YAML
appId: com.example.myapp
productName: MyApp
directories:
output: release
files:
- "!**/*.map"
publish:
provider: generic
url: https://updates.example.com/win
channel: latest

这段配置的意思是:使用 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 配置是这样的:

YAML
mac:
target:
- dmg
- zip
category: public.app-category.developer-tools
 
win:
target:
- nsis
 
linux:
target:
- AppImage

Windows 这边,NSIS 是 electron-updater 支持最完整的安装格式,支持差分更新,体验最顺。Linux 下 AppImage 是最常见的选择,但更新完需要重启应用才能生效,这个限制要提前跟用户讲清楚。

2.3 artifactName 的命名与坑

安装包文件名不是随便起的。electron-updater 下载时会把 latest.yml 里的 path 拼到 URL 后面,如果文件名里带了空格,URL 编码处理不好就会 404。更严重的情况是多个平台产物重名,导致服务器上文件互相覆盖。

我的 artifactName 固定写法:

YAML
artifactName: ${name}-${version}-${os}-${arch}.${ext}
win:
artifactName: ${name}-${version}-setup.exe
mac:
artifactName: ${name}-${version}-mac.zip
linux:
artifactName: ${name}-${version}.AppImage

${name} 取自 package.json 的 name,${version} 是版本号。这样生成的文件名唯一且可读,发布到服务器后一眼就知道是哪个版本。命名规范这件事,看起来不起眼,真出了“覆盖旧包导致更新失败”的问题时,你会回来感谢这一节的。

3. latest.yml 字段拆解:版本比较、文件校验与增量更新

3.1 字段逐个看

打包完成后,electron-builder 会在输出目录生成 latest.yml。Windows 下长这样:

YAML
version: 1.1.0
files:
- url: MyApp-1.1.0-setup.exe
sha512: 9K7U4HnX4oU4vM4FQx8Hc1qWf6Ib0Xp3N2d+yJyZ9eA35t3G5m7n2JpE7GB5V2Q==
size: 84736491
path: MyApp-1.1.0-setup.exe
sha512: 9K7U4HnX4oU4vM4FQx8Hc1qWf6Ib0Xp3N2d+yJyZ9eA35t3G5m7n2JpE7GB5V2Q==
releaseDate: '2024-08-01T10:00:00.000Z'

逐个解释:

  • 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 下。检查已安装应用时,可以用这个命令确认文件是否正常:

BASH
# Windows 下到安装目录
dir resources\app-update.yml
 
# macOS 下
cat "/Applications/MyApp.app/Contents/Resources/app-update.yml"

如果安装后找不到这个文件,大概率是打包配置出了问题,自动更新绝对跑不起来,先解决这个再排查别的。

4.2 一份典型 app-update.yml 的内容与解释

实际内容因平台而异,Windows 下大致长这样:

YAML
owner: myteam
publisher: myteam
updaterCacheDirName: myapp-updater
provider: generic
url: https://updates.example.com/win
channel: latest

provider 和 url 是核心,provider 决定 electron-updater 用哪种更新协议。generic 就是直接访问静态文件服务器;如果配成 github,它就会走 GitHub Releases 的 API。updaterCacheDirName 决定了更新缓存的目录名,这个字段在排查下载失败问题时很有用。

4.3 开发调试更新时的常见玩法

调试自动更新最难受的是,打包一次、安装一次、验证一次,周期太长。我通常的做法是本地起一个静态文件服务器,把 publish 的 URL 临时指向 localhost:

YAML
publish:
provider: generic
url: http://127.0.0.1:8080/win
channel: latest

打包安装后,electron-updater 会从本地服务器拉取清单。配合 autoUpdater.setFeedURL 动态覆盖 URL,也可以实现类似效果。但注意,macOS 对非 HTTPS 的更新源限制很严,本地调试 mac 更新时,建议用 http 配合 debug 模式,或者起一个带自签证书的 HTTPS 服务,否则会卡在请求阶段。

4.4 “改了服务器地址不生效”的踩坑

这是被问得最多的问题之一。现象是:代码里把 publish 的 url 改成了新服务器,重新打包也打了,但线上用户永远在请求旧地址。

原因前面讲过:app-update.yml 是打包时生成的,它记录的是打包那一刻的配置。已经安装在用户机器上的版本,不会因为你后来改代码而改变。要让存量用户切换到新服务器,有两个办法:

  1. 发布一个新版本,让用户在旧地址上还能拉到这个新包,再带着新 url 覆盖安装。
  2. 如果旧地址完全不可用,只能让用户手动下载新安装包安装,没有捷径。

所以,更新服务器域名和地址,一定要在用户量还小的时候定下来,切换成本会随着用户数增长而急剧上升。

5. 自建更新服务器:目录结构、HTTPS 与灰度放量

5.1 为什么自建服务器只需要静态文件

electron-updater 的 generic provider 是最简单的更新源类型:它只需要一个能下载文件的静态服务器,不需要任何后端逻辑,不用数据库,不用接口。这也意味着,Nginx、OSS、甚至一个简单的 Python http.server 都能当更新源。

和 GitHub Releases 相比,自建服务器最大的优势是没有平台限制和网络限制。GitHub 在国内的访问稳定性、私有仓库的下载鉴权、单文件 2GB 大小限制,都会在某个时间点变成团队的痛点。自建一台小带宽的 VPS 做文件服务,成本不高,但自由度很高。

5.2 推荐的发布目录结构

目录结构决定了后期维护时会不会手忙脚乱。按平台分目录,每个目录放各自的清单和安装包,是我目前最推荐的结构:

TEXT
/opt/updates/
├── win/
│ ├── latest.yml
│ ├── MyApp-1.1.0-setup.exe
│ └── archive/
│ └── MyApp-1.0.0-setup.exe
├── mac/
│ ├── latest-mac.yml
│ ├── MyApp-1.1.0-mac.zip
│ └── archive/
│ └── MyApp-1.0.0-mac.zip
└── linux/
├── latest-linux.yml
├── MyApp-1.1.0.AppImage
└── archive/
└── MyApp-1.0.0.AppImage

发布流程很简单:

  1. 本地执行 electron-builder 打包,产物复制到对应平台目录的根目录。
  2. 清清单文件覆盖到同目录。
  3. 旧版本安装包手动移入 archive,如果要回滚,把旧包从 archive 复制回来,同时恢复旧版清单。

注意 mac 目录下的清单文件名是 latest-mac.yml 而不是 latest.yml,Linux 是 latest-linux.yml。Windows 才是 latest.yml。这个差异来自 electron-builder 多平台构建的设计,目的就是避免多个平台共享服务器目录时同名文件互相覆盖。

5.3 Nginx 配置示例与流量注意

自建更新服务器的核心就是 Nginx 静态服务,配置很轻量:

NGINX
server {
listen 443 ssl;
server_name updates.example.com;
 
ssl_certificate /etc/letsencrypt/live/updates.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/updates.example.com/privkey.pem;
 
root /opt/updates;
autoindex off;
 
# 清单文件不缓存,避免客户端拿到旧的 latest.yml
location ~* \.(ya?ml)$ {
add_header Cache-Control "no-cache";
try_files $uri =404;
}
 
# 安装包允许浏览器和更新器缓存,静态文件上 CDN 也方便
location ~* \.(exe|zip|AppImage|blockmap)$ {
add_header Cache-Control "public, max-age=604800";
try_files $uri =404;
}
 
client_max_body_size 512m;
}

大文件下载建议保留 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:

YAML
publish:
provider: generic
url: https://updates.example.com/win
channel: beta

对应的客户端在打包安装后,会请求 https://updates.example.com/win/beta.yml。

生产环境的稳定版客户端请求 latest.yml,测试用户请求 beta.yml。两个清单指向同一个安装包或不同安装包都行。想全量放量时,把 beta.yml 的内容同步到 latest.yml 即可。

整个过程不需要任何后端代码,只需要在发布目录里多放一份 yml 文件。这是自建服务器对比第三方托管服务最实在的优势之一。

6. 上线后实测遇到的三类问题与排查链路

6.1 症状:应用一直提示“已是最新版本”

这是出现频率最高的问题。先说排查顺序,这是我在团队内部框定的标准链路:

  1. 打开已安装应用,确认 app-update.yml 存在,且 url、channel 正确。
  2. 用 curl 请求服务端清单,确认服务器上确实是最新版本且文件可访问。
  3. 对比清单里的 version 和本地 package.json 里的 version,确认远端版本号高于本地。
  4. 确认你测试的是安装包,而不是用 electron . 直接跑源码。开发模式不会走到真实的 app-update.yml。
  5. 检查服务端 latest.yml 有没有被缓存。如果用了 CDN 或 Nginx 代理,清理缓存再看一遍。

实际案例里,我遇到过最隐蔽的一次是 Nginx 对 latest.yml 的 location 匹配没生效,因为前面的 location / 块兜底返回了带缓存头的响应。客户端拿到旧的清单文件,永远认为本地已经是最新。修法是显式声明清单文件的 Cache-Control,也就是 5.3 节里的配置。

6.2 症状:macOS 更新后提示应用已损坏

macOS 更新后的“已损坏”提示,核心原因基本都集中在签名和公证上。自动更新下载的是 zip 包,更新时替换整个 .app。如果 zip 里的 .app 没有正确签名,或者签名机和构建机不一致,更新后 Gatekeeper 就会拒绝运行。

排查命令是这样:

BASH
codesign -dv "/Applications/MyApp.app"
spctl --assess --type execute -vv "/Applications/MyApp.app"

如果 codesign 输出里没有权威的 Team ID,说明签名没配好。macOS 的 electron-builder 配置里,需要确保 build 机配置了证书,同时开启 notarization(公证):

YAML
mac:
notarize:
teamId: XXXXXXXXXX

如果没有企业开发者账号,建议在 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 事件,把错误信息透出到界面上,而不是让用户面对一个静默失败的黑盒:

JS
autoUpdater.on('error', (err) => {
dialog.showErrorBox('更新失败', `请检查网络后重试。\n\n详情:${err.message}`);
});

把错误暴露出来,用户反馈问题时,你也能从错误信息里快速定位到是下载阶段还是安装阶段出了岔子。

7. 发布前记得做一次清单核对

最后分享一个我现在每次发版都会执行的动作。工具自动生成的清单文件虽然可靠,但发布链路里往往存在人工操作,比如手动上传、手动覆盖文件。这些环节最容易出错。

我的核对清单是这样的:

  • [ ] 确认 CI 构建生成的 latest.yml / latest-mac.yml / latest-linux.yml 与安装包在同一个目录。
  • [ ] 用命令对比本地打包输出目录和服务器目录里的 sha512 是否一致。
  • [ ] 确认服务器清单文件的版本号高于上一个已发布版本。
  • [ ] 在一台干净环境的测试机上安装全新包,确认 app-update.yml 里的 URL 正确。
  • [ ] 触发一次检查更新,观察是否存在 HTTP 缓存导致旧清单的问题。

多平台发布时,建议把各平台的清单文件名和产物列成一张表,贴在 CI 配置文件旁边。团队里每个人发布前都过一遍,比出了问题再回滚舒服得多。自动更新这条路,搭起来不难,真正难的是后续每个版本迭代时不出岔子。希望这篇实操指南能帮你把这条链路从一开始就理清楚。