Electron自动更新全攻略:electron-builder配置与自建更新服务器实战
写 Electron 客户端最绕不开的坎,就是打包和自动更新。本地跑得再欢,一到分发环节,各种签名、产物路径、更新机制的问题全冒出来了。很多团队做到一半卡住,不是卡在 electron-builder 配置上,就是卡在自建更新服务器和 latest.yml、app-update.yml 这两个文件的配合上。这篇文章就把这条链路完整捋一遍,从打包配置到自建更新服务,再到客户端集成,全部基于我在生产环境里实际跑通的方案,适合正在维护 Electron 桌面应用、以及准备把自动更新从第三方托管迁到自己服务器上的开发者参考。
1. 方案整体设计与选型拆解
1.1 为什么选 electron-builder 而不是其他方案
Electron 打包工具目前主流的有 electron-builder、electron-packager、Electron Forge。Electron Forge 是官方推荐的,集成了打包和发布流程,但它对自动更新的支持更偏商业化托管,自建服务器的配置不如 electron-builder 灵活。electron-packager 则只是个裸打包工具,产物连安装包都不是,更别提生成更新元数据了。
electron-builder 的优势是它把打包、签名、生成更新元数据(latest.yml、latest-mac.yml)一条龙处理了。只要在配置文件里写好 publish 相关设置,构建完成后自动生成对应的更新描述文件,这对于做自动更新来说是决定性的效率提升。我在多个项目里对比过,electron-builder 生成的 NSIS 安装包、dmg、AppImage 产物都很标准,配合 electron-updater 使用几乎零阻力。
1.2 自建更新服务器 vs 第三方托管
Electron 应用默认可以用 GitHub Releases 做更新源,但你得考虑国内网络环境的下载速度。一个 80MB 的安装包走 GitHub 不定时抽风,用户那边更新失败率会直接让产品口碑崩掉。自建更新服务器最大优势是可控:
- 更新包放在自己的 CDN 或内网服务器,下载速度快且稳定
- 更新策略可以自己控制,比如强制更新、灰度发布
- 不依赖第三方平台的接口稳定性
- 企业内网环境部署时,自建服务器是唯一可选方案
自建服务器的形态也很灵活,可以是一个 Nginx 静态目录,也可以是一个带鉴权的 Node.js 服务。关键是让 electron-updater 能通过 HTTP 请求拿到 latest.yml 和安装包文件。
1.3 latest.yml 与 app-update.yml 到底什么关系
很多人被这两个文件绕晕,我直接说结论:
latest.yml是构建时生成的更新元数据文件,放在你的更新服务器上,客户端每次检查更新时请求的就是它app-update.yml是打包时嵌入到安装包(其实是resources目录)里的本地配置文件,告诉客户端应该去哪查更新、查哪个文件
app-update.yml 的内容会包含你配置的 publish 地址。如果用 electron-builder 的 electron-updater,它启动时会读这个内嵌文件,然后远程请求 latest.yml 对比版本号。
有一点要强调:老版本 electron-updater 有时需要你手动在 dev-app-update.yml 里配置 url 才能在开发环境调试更新,但在打包产物里并不需要单独创建 app-update.yml,electron-builder 会自动把它打进 resources 目录。这里只要保证 build 配置正确即可,不要手动干预 app-update.yml 的内容。
2. electron-builder 打包配置实战
2.1 一份可直接落地的 electron-builder.yml
直接给一份我常用的配置,对应的 electron-builder 版本是 24.x 以上。
重点说几个配置:
publish.provider设为generic,这是 electron-updater 支持自建服务器的关键。url 直接指向你更新服务器的根地址,后面客户端请求latest.yml时会拼成https://update.example.com/releases/latest.ymlartifactName里的${version}占位符很重要,如果你不给它定义,electron-builder 默认的命名里有${channel},在非 stable 分支下可能会生成带 channel 的安装包文件名,导致 latest.yml 里记录的 url 和你服务器上实际的文件名对不上directories.output我习惯统一输出到 release 目录,方便 CI 打包后直接上传整个目录
2.2 Windows NSIS 打包与代码签名
Windows 平台的自动更新绕不开 NSIS 安装器。electron-updater 在 Windows 下更新时,依赖安装包去覆盖旧的安装目录,所以产物必须是 NSIS 安装包。不要考虑用 portable 目标做自动更新,免安装版没有注册安装路径,更新完没法覆盖旧版本。
NSIS 的几个关键参数:
oneClick: false让用户可以看到安装路径选择,对国内用户更友好perMachine: false安装到当前用户目录,避免权限问题。如果做企业级软件需要全机器安装,再改成 true,但那样安装时需要管理员权限,更新时也会有 UAC 弹窗allowToChangeInstallationDirectory: true允许改安装目录,但有一个副作用,如果用户改过安装目录,electron-updater 需要注册表里的InstallLocation或UninstallString来定位安装路径,否则更新会找不到旧版本。electron-builder 默认会写这些注册表项,正常使用没问题
代码签名这块我要专门强调:Windows 上如果不签名,electron-updater 更新时会触发 SmartScreen 拦截,用户还得额外点击“仍要运行”。企业环境里更麻烦,直接可能被安全策略拦截。签名证书建议用正规机构签发的 EV 或 OV 代码签名证书,有条件的走一下微软的签名认证减少误报。签名操作是在打包时自动做的,配置里加:
实际打包时用环境变量传密码,不要硬编码进配置文件。
2.3 构建产物里哪些文件需要上传
构建完成之后,release 目录下会生成一堆文件。以 Windows 为例,典型的产物有:
ExampleApp-1.0.0-x64.exe(实际安装包)latest.ymlbuilder-effective-config.yaml(调试用)win-unpacked/(免安装目录,开发调试用)
上传到更新服务器时,只需要传安装包和对应的 yml 文件。win-unpacked 不用传,那只是未打包的调试目录。
macOS 构建时生成 latest-mac.yml,Linux 生成 latest-linux.yml。这是因为不同平台 electron-updater 会请求不同名字的元数据文件,Windows 固定请求 latest.yml。
3. latest.yml 与 app-update.yml 文件结构深入
3.1 文件内容逐字段解析
先看一份真实的 latest.yml:
字段说明:
version是安装包的版本号,严格对应 package.json 里的 version。electron-updater 会比较这个版本和本地app.getVersion()的结果,语义化版本比较用的是 electron-builder 内部的 semver 逻辑files数组里的 url 是文件名(相对路径),electron-updater 会基于你配置的 provider url 拼出完整下载地址sha512是 Base64 编码的 SHA512 哈希,用于校验下载的安装包是否完整。这是自动更新安全性的重要一环,防的就是下载文件被中间人篡改或损坏releaseDate是发布时间,electron-updater 在某些场景下会用它在本地缓存更新信息
path 字段和顶层 sha512 是兼容旧版本的冗余字段,新版本 Electron 用 files 数组里的信息,但生成时都会带上。
3.2 app-update.yml 在打包后去哪了
打包完成后,resources/app-update.yml 内容大概是这样:
这个文件是 electron-builder 根据你的 publish 配置自动生成的。客户端运行时,electron-updater 会读它拿到 provider 和 url,然后去请求 latest.yml。
这里有个细节:updaterCacheDirName 决定了更新缓存的目录名,在 Windows 上是 %LOCALAPPDATA%/<updaterCacheDirName>,macOS 上是 ~/Library/Caches/<updaterCacheDirName>。如果你发现更新下载到一半的临时文件一直占着磁盘空间,就是从这个目录找。想手动清除,把对应目录删掉即可。
3.3 手动生成 latest.yml 的两种方式
正常情况下 electron-builder 构建完成后会自动生成 latest.yml,不需要手动维护。但有一些场景需要你手动处理:
场景一:服务端已经有安装包,想补一份更新元数据。可以用 Node.js 计算 sha512:
然后拼一个 yaml 文件,注意 version、path、sha512 三个字段必须准确。
场景二:你的更新服务器需要动态生成 latest.yml(比如做灰度发布时给不同用户返回不同版本),这时服务端放一个模板,根据请求参数动态渲染 yaml 返回即可,但要注意 Content-Type 必须能被 electron-updater 正确解析。
3.4 sha512 校验的防篡改原理
electron-updater 下载完安装包后,会先计算本地文件的 SHA512,再和 latest.yml 里的值比对,不一致就直接判更新失败。这个机制是为了防止中间人攻击和下载损坏。
实际中常见的问题是:Nginx 开启 gzip 后,如果配置错误导致动态请求被压缩,可能影响校验。不过静态文件一般不会被 gzip 处理,所以问题更多出现在自建 Node 服务没正确返回文件流,或 CDN 回源超时导致文件截断。后面排查章节细讲。
4. 自建更新服务器完整实现
4.1 纯静态 Nginx 部署方案
如果更新包是固定的、不需要动态分发,最省事的方案是直接用 Nginx 托管 release 目录。下面是一个我实测可用的配置:
几个关键点:
default_type text/yaml要刻意设置,Nginx 默认对 yml 后缀可能返回application/octet-stream,某些网络环境下可能导致客户端解析失败- yml 文件要禁用缓存(
no-cache),这样客户端每轮检查更新时能拿到最新的版本信息。安装包这类大文件则可以开长缓存,因为文件名里带版本号,新版本自然会生成新 URL,不存在缓存污染问题 autoindex off关闭目录列表,避免敏感文件被扫描- 如果更新服务器走 HTTPS,electron-updater 对证书校验很严格,自签名证书会导致请求直接失败。要么用正式证书,要么在代码里临时关掉校验(生产环境不建议)
4.2 Node.js 极简更新服务器
如果后续要做灰度发布、限量放量、鉴权,静态 Nginx 就不够用了。这里给一个基于 Express 的最小实现:
express 的 sendFile 天然支持 Range 请求,electron-updater 下载大安装包时分段拉取,不支持 Range 的服务器会导致下载中断后无法续传,用户看着进度条卡住然后失败。这一点在自建服务器时最容易踩坑。
生产环境建议加一层鉴权,比如在 yml 接口加签名校验参数,或者用 IP 白名单控制灰度用户范围。
4.3 electron-updater 客户端集成
主进程引入 electron-updater:
几个设计决策:
autoDownload = false的目的是把“是否更新”的决策权交给用户。先收到update-available事件,弹窗提示,用户确认后才调用downloadUpdate()。对于流量敏感的小团队产品,避免用户一进应用就被迫下载大更新包- 不要把
checkForUpdates放在 ready 里立刻执行,等几秒让 UI 先起来,否则更新检查可能阻塞启动流程 update-downloaded之后不要立刻quitAndInstall(),应该提示用户保存工作。electron-updater 的quitAndInstall默认isSilent=false,会弹系统安装界面做正式安装
4.4 强制更新与灰度发布设计
生产环境经常会遇到“旧版本必须升级才能继续用”的需求。我通常的做法是写一个版本策略接口:
客户端检查更新的流程变为:
- 启动时请求版本策略接口
- 如果本地版本低于
latest,走正常更新流程,非强制 - 如果本地版本低于
minimum,标记为强制更新。用户在对话框里只有“立即更新”按钮,没有“跳过”选项,更新的过程中 UI 提示不能关闭应用
强制更新时,update-downloaded 后直接 autoUpdater.quitAndInstall(true, true),静默安装并自动重启。注意这里有个坑:NSIS 安装器在静默安装时,如果旧进程没有完全退出,安装会失败。稳妥做法是安装前先让应用退出所有子进程。
灰度发布可以通过服务端做:不同 user agent 或请求 IP 返回不同版本的 latest.yml。在 Node 更新服务器里加一层判断即可。
5. 常见问题与排查技巧实录
5.1 更新请求 404 或报错网络异常
现象:客户端日志显示访问 latest.yml 返回 404,或者 getLatestVersion 处抛 Network Error。
排查:
- 浏览器直接访问
https://你的域名/releases/latest.yml,看能不能正常返回内容 - 检查 app-update.yml 里的 url 是否和实际服务器路径一致。这块不一致的原因多半是在 electron-builder 配置里改了
publish.url但没重新打包 - 服务器路径大小写问题。Linux 服务器要严格区分文件名大小写,
Latest.yml和latest.yml是两个文件
5.2 sha512 mismatch 更新永远失败
现象:下载完成后报 ERR_UPDATER_SHA512_MISMATCH。
常见原因:服务器开启了响应压缩导致文件流被修改;或 Nginx 的 gzip_static 配置错误返回了 .gz 文件;或 CDN 回源时做了缓存版本冲突。
解决办法:先在服务器上手动下载安装包,本地计算 sha512 再和 latest.yml 对比。如果一致但客户端仍然报错,重点检查反代层是否对 .exe 文件做了额外的编码转换。给下载路径加上 proxy_buffering off; 或等效配置,让文件流直通。
5.3 下载到一半失败,重新下载还是失败
现象:更新下载进度走到 40% 中断,重试依旧。
绝大多数情况是更新服务器不支持 Range 请求。验证方式很简单:
如果响应头里没有 Accept-Ranges: bytes 或 Content-Range,说明服务端没有正确处理分片请求。Nginx 默认支持 Range,Node 的 res.sendFile 也支持,CDN 需要确认配置里开启了分片回源。
5.4 Windows 更新后旧进程还在,安装失败
现象:提示安装失败,或更新完成后应用没有重启,进程还留在任务管理器里。
NSIS 在覆盖安装前要求目标进程退出。Electron 主进程在 update-downloaded 后还有子进程在跑(比如渲染进程、GPU 进程、某些辅助进程),quitAndInstall 会尝试关闭主进程,但如果应用里有守护进程或托盘进程没有退出,就会卡住。
我的做法是:在 quitAndInstall 前先主动退出所有子进程,然后延迟 300ms 再调用安装函数。同时监听 window-all-closed 事件,确保 app.quit() 真正让主进程退出。
5.5 更新包下载慢
如果 Nginx 部署在国内服务器,跨地域访问确实会慢,毕竟大文件传输的瓶颈在带宽。排查方法:先 ping 看延迟,再确认运营商带宽。小团队建议直接套 CDN,只对 yml 文件禁用缓存,exe 文件走 CDN 加速。也可以在客户端做流量控制,比如把 autoUpdater 的下载并发连接数调低,避免用户网络的带宽被占满。
5.6 macOS 提示无法验证开发者
macOS 上如果应用没做公证(Notarization),用户打开更新后的应用会被 Gatekeeper 拦截。electron-builder 配置里需要开启 notarize:
注意 macOS 更新包下载完成后会先挂载 dmg 再提取 app,如果签名或公证不过,安装过程会静默失败,用户看起来像是更新了但版本没变。这个坑排查时很难发现,需要打开控制台看具体错误。
5.7 强制更新时应用无法正常重启
Electron 的 quitAndInstall 在某些 Windows 环境下不会立刻安装,而是等应用退出后才触发安装器。如果业务里有“保存用户数据”的需求,一定要在安装前完成数据提交和持久化,否则数据丢失的锅就扣到更新头上了。我把这个流程统一封装成一个 restartAfterUpdated() 函数,先 flush 本地数据,再退出。
releaseSingleInstanceLock 解决的是单实例锁冲突问题。Electron 单实例锁在更新安装时会让安装器卡住,因为系统发现应用还在运行,锁没有被释放。
5.8 如何快速定位问题:electron-updater 日志
electron-updater 会把日志打到 stderr,但默认不开启详情。我在集成时把日志转发到一个本地文件:
排查时直接看用户目录下的 updater.log,能定位绝大部分请求、响应、校验过程的问题。
5.9 版本号回退的坑
不要在生产环境把版本号从 2.0.0 回退到 1.9.9。electron-updater 用 semver 比较,本地版本高于服务器版本时会在 update-not-available 分支直接结束,不会再有后续更新。一旦发出版本号倒挂,所有 2.0.0 用户会永久停留在这个版本,只能手动覆盖安装。发布流程里记得加一道校验,deny 版本号递减的 release。
6. 更新策略与实操体验补充
6.1 增量更新不是必需品
很多团队一上来就想要增量更新,控制更新包体积。但 electron-builder 的 NSIS 安装包本身已经基于 zip 压缩,内部文件变化较小时,新安装包和旧安装包之间其实存在可复用块。electron-updater 支持 blockmap 差分更新,通过 electron-builder 构建时会默认生成 latest.yml 同名的 .blockmap 文件。如果构建输出里能看到 ExampleApp-1.0.1-x64.exe.blockmap,上传服务器时把它一并带上,客户端就能自动尝试增量下载,下载量会显著减小。
blockmap 的匹配机制是基于文件内容的,不适合跨大版本发布。如果你每次更新都改了大量依赖,增量效率低,客户端会自动回退到全量下载,不需要额外处理。
6.2 发布流程的规范化
更新服务器的文件结构建议保持版本化目录:
旧版本安装包不要急着删。用户可能停留在多个旧版本,某些场景需要手动回滚时,旧包还在服务器上就有兜底。我一般只保留最近 5 个版本的安装包,历史包放到冷存储。
CI 里做好流水线后,每次打 tag 自动触发构建、签名、上传、覆盖 latest.yml。人工操作环节越多,出错概率越大,更新服务器这种安全敏感链路上要用自动化消除变量。
6.3 关于灰度发布的优先级
灰度发布确实能让事故影响面变小,但代价是维护一套分发规则。小团队产品不必一上来就做灰度,先保证“全量发布快速回滚”的能力更重要。回滚比灰度等级高。需要灰度时,用 request header 或 query 参数里带灰度分组 id,服务端逻辑简单可控。
6.4 最后说点实在的
我在实际跑更新链路时最深的体会是:更新服务器的稳定性比功能复杂度重要得多。你既想做得“智能”,又要防住网络的不确定性,最后发现真正能救命的还是可靠的文件托管、合理的缓存控制、以及完整的日志。用好 xx 文件、把下载路径的 Range 支持验证到位,已经能解决 80% 的更新问题。剩下的,就是发布前手动刷一次全流程,用真实安装包从旧版本升到新版本,别偷懒。