Electron 应用打包与自建更新服务器实战:electron-builder 配置详解
1. 桌面应用打包与更新,为什么值得单独拎出来讲
Electron 这个技术栈,入门门槛确实低。会写前端三件套,再懂一点 Node.js,就能把网页塞进一个桌面壳子里跑起来。但真正把应用交付到用户手上,尤其是要持续迭代、持续修复线上问题的时候,打包和更新这两件事,才是区分“能跑”和“能用”的分水岭。
我见过太多团队,开发阶段顺风顺水,一到发版就手忙脚乱:Windows 上装完打不开、macOS 提示应用已损坏、用户装了旧版本死活收不到新版本推送、更新服务器一挂全员卡在启动页。这些问题不是代码逻辑的问题,而是打包配置和更新链路的问题。它们平时不显山不露水,一旦爆发就是线上事故。
这篇内容围绕 electron-builder 这个打包工具,把 latest.yml、app-update.yml 这两个更新配置文件的作用讲透,再手把手带你搭一套自建更新服务器。整套方案不依赖任何第三方托管服务,更新逻辑完全掌握在自己手里,适合对数据流向有要求、或者需要内网部署的团队。无论你是刚接触 Electron 打包的新手,还是已经用过 electron-builder 但没深究过更新机制的老手,都能从里面找到可以直接抄作业的配置和踩坑经验。
我个人的习惯是:打包配置和更新配置必须一起设计,不能先打包再想更新。因为更新能不能成功,很大程度上取决于打包阶段生成的元数据文件是否正确。这两件事是绑死的,拆开看容易漏掉关键细节。
2. 打包工具选型:为什么是 electron-builder
2.1 主流方案横向对比
Electron 应用的打包工具不止一个,常见的有 electron-builder、electron-forge、electron-packager 这几种。我早期用过 electron-packager,它只负责把 Electron 运行时和你的代码塞进一个目录,生成可执行文件,但安装包制作、签名、更新元数据这些它一概不管。electron-forge 是官方维护的,生态整合得不错,但它的更新方案偏向配合 Squirrel 和第三方发布平台,自建更新服务器的文档相对零散。
electron-builder 的优势在于它把打包、安装包生成、代码签名、更新元数据生成这几件事串成了一条流水线。你只需要在 package.json 或者单独的配置文件里声明目标平台和格式,它就能一次性产出 Windows 的 NSIS 安装包、macOS 的 dmg 和 zip、Linux 的 AppImage 或 deb,同时自动生成更新所需的 latest.yml 和 app-update.yml。这种“一站式”能力,对于需要同时维护多平台版本的团队来说,省掉了很多胶水代码。
提示:electron-builder 的版本迭代比较快,建议锁定一个稳定版本,不要盲目追最新。我目前用的是 24.x 系列,配置格式和更新行为都比较稳定。
2.2 核心配置项拆解
electron-builder 的配置可以写在 package.json 的 build 字段里,也可以单独放在 electron-builder.yml 或 electron-builder.json 中。我倾向于单独放一个 electron-builder.yml,因为配置项多了以后,package.json 会变得非常臃肿,而且 YAML 格式支持注释,方便团队协作时标注每个参数的含义。
几个关键配置项需要重点理解:
- appId:应用的唯一标识,Windows 上会写入注册表,macOS 上影响钥匙串访问。一旦发布就不能随意更改,否则更新会识别成两个不同的应用。
- productName:安装后显示的应用名称,可以带空格和中文,但建议用英文,避免某些系统下的编码问题。
- directories.output:打包产物的输出目录,默认是
dist。建议显式指定,方便 CI 流水线收集产物。 - files:声明哪些文件需要打进安装包。默认会包含所有文件,但
node_modules里的开发依赖、测试文件、源码映射这些不应该打进去,需要显式排除。 - win/mac/linux:各平台特有的配置,比如图标、目标格式、签名证书路径等。
- publish:更新服务器的配置,这个直接决定了
app-update.yml的内容。
我踩过的一个坑是 files 配置写得太宽松,把整个 node_modules 都打进去了,结果安装包体积从 80MB 膨胀到 300MB。后来改成白名单模式,只包含 dist、main.js、package.json 和必要的原生模块,体积立刻降下来了。打包体积直接影响用户的下载意愿和更新速度,这个优化值得花时间。
2.3 多平台打包的现实约束
理论上 electron-builder 可以交叉编译,比如在 macOS 上打 Windows 包。但实际操作中,Windows 的代码签名工具、macOS 的公证流程,都强烈依赖原生环境。我的建议是:Windows 包在 Windows 机器或 Windows 容器里打,macOS 包在 macOS 机器上打,Linux 包在 Linux 环境里打。用 CI 流水线并行跑三个平台的任务,比在一台机器上折腾交叉编译要省心得多。
如果团队没有 macOS 设备,可以考虑用云端的 macOS 构建服务,但要注意代码签名证书的安全管理。证书文件绝对不能提交到代码仓库,应该通过 CI 的环境变量或者密钥管理服务注入。
3. latest.yml 与 app-update.yml:更新链路的两个关键文件
3.1 latest.yml 到底存了什么
latest.yml 是 electron-builder 在打包时自动生成的更新元数据文件,它会被放到更新服务器的指定目录下。客户端启动时,会去请求这个文件,对比版本号,决定是否需要下载新版本。
一个典型的 latest.yml 内容长这样:
这里有几个字段需要重点关注:
- version:最新版本号,客户端拿它和本地版本对比。版本号必须符合语义化版本规范,否则比较逻辑会出错。
- files:更新文件的列表,包含文件名、SHA512 校验值和文件大小。客户端下载完成后会用 SHA512 校验文件完整性,校验不通过会重新下载。
- path:主更新文件的路径,通常是安装包或者增量更新包。
- releaseDate:发布时间,用于某些更新策略下的时间判断。
latest.yml 的生成是自动的,但它的内容正确性依赖于打包配置。比如 publish 里的 url 字段如果写错了,生成的 latest.yml 里的下载地址就会指向错误的位置,客户端自然更新失败。
3.2 app-update.yml 的角色
app-update.yml 是打进安装包内部的一个配置文件,它告诉客户端去哪里检查更新。这个文件在打包时生成,内容大致如下:
- provider:更新提供方类型。自建服务器用
generic,表示通过 HTTP 直接访问静态文件。 - url:更新服务器的根地址,客户端会在这个地址后面拼接
latest.yml的路径。 - channel:更新通道,默认是
latest。如果你有灰度发布的需求,可以设置不同的通道,比如beta、stable。
app-update.yml 和 latest.yml 的关系是:前者告诉客户端“去哪里找”,后者告诉客户端“找到了什么”。两者配合,才能完成一次完整的更新检查。
注意:
app-update.yml是打包时写死的,一旦发布就无法远程修改。如果更新服务器地址变了,旧版本客户端就再也收不到更新了。所以这个地址必须提前规划好,最好用一个稳定的域名,不要用 IP 地址或者临时域名。
3.3 两个文件的协作流程
客户端启动时的更新检查流程大致是这样的:
- 读取安装包内的
app-update.yml,获取更新服务器地址和通道。 - 向
${url}/${channel}.yml发起请求,也就是latest.yml。 - 解析
latest.yml,拿到最新版本号,和本地版本对比。 - 如果远程版本更高,下载
files里列出的更新文件。 - 下载完成后校验 SHA512,校验通过则触发安装流程。
这个流程里,任何一个环节出问题都会导致更新失败。比如服务器返回 404,说明 latest.yml 没放对位置;SHA512 不匹配,说明文件在传输过程中损坏或者被篡改;版本号比较异常,说明版本号格式不规范。
4. 自建更新服务器:从零搭建到上线
4.1 为什么选择自建
用第三方托管服务做更新,省事但有局限。一是数据要经过第三方服务器,某些场景下不合适;二是免费额度有限,用户量大了要付费;三是定制化能力弱,比如你想根据用户地域返回不同的更新包,或者做灰度发布,第三方服务往往不支持。
自建更新服务器的本质很简单:一个能通过 HTTP 访问的静态文件目录。electron-builder 生成的 latest.yml 和安装包文件放进去,配好 MIME 类型和缓存策略,就能工作。技术栈上,Nginx、Apache、甚至一个 Node.js 的静态文件服务都行。我用 Nginx,因为它配置简单、性能好、日志清晰。
4.2 服务器目录结构设计
目录结构直接影响更新地址的拼接逻辑。我推荐的目录结构是这样的:
所有文件平铺在一个目录下,latest.yml 里的 url 字段只写文件名,客户端会自动拼接 app-update.yml 里的 url 前缀。这种扁平结构最简单,不容易出错。
如果要做多通道,可以按通道分目录:
对应的 app-update.yml 里 channel 分别设为 latest 和 beta,客户端就会去请求不同目录下的 latest.yml。
4.3 Nginx 配置要点
Nginx 的配置有几个关键点:
这里有两个设计考量:latest.yml 必须实时获取,不能缓存,否则用户永远看不到新版本;安装包文件因为文件名带版本号,内容不会变,可以放心地长期缓存,减少服务器带宽压力。
提示:HTTPS 是必须的。electron-builder 的更新机制在非 HTTPS 环境下会有安全警告,而且某些系统版本会直接拒绝更新。证书可以用 Let's Encrypt 免费申请,自动续期。
4.4 上传与发布流程自动化
手动上传文件容易出错,尤其是多平台多版本的时候。我写了一个简单的发布脚本,用 Node.js 实现,核心逻辑是:
这个脚本可以集成到 CI 流水线里,打包完成后自动上传。注意 latest.yml 要最后上传,避免客户端在安装包还没传完的时候就检测到新版本,导致下载失败。
5. 实操全流程:从打包到更新验证
5.1 环境准备与依赖安装
先确保 Node.js 版本在 16 以上,然后安装 electron-builder:
在 package.json 里添加打包脚本:
5.2 electron-builder.yml 完整配置示例
这份配置里,publish 部分决定了 app-update.yml 的内容。url 指向更新服务器的 latest 目录,channel 是 latest,客户端就会去请求 https://update.example.com/releases/latest/latest.yml。
5.3 打包产物解析
执行 npm run dist 后,dist 目录下会生成这些文件:
| 文件名 | 作用 |
|---|---|
| MyApp-Setup-1.2.3.exe | Windows 安装包 |
| MyApp-Setup-1.2.3.exe.blockmap | 增量更新用的块映射文件 |
| latest.yml | Windows 平台的更新元数据 |
| MyApp-1.2.3-mac.zip | macOS 更新包 |
| MyApp-1.2.3.dmg | macOS 安装镜像 |
| mac/latest-mac.yml | macOS 平台的更新元数据 |
注意 Windows 和 macOS 的更新元数据文件名不同,Windows 是 latest.yml,macOS 是 latest-mac.yml。上传的时候要区分开,放到对应的目录或者用不同的文件名。
5.4 客户端更新代码集成
在主进程里引入 electron-updater:
这里我把 autoDownload 设为 false,让用户决定是否下载。有些团队喜欢静默下载,下载完再提示安装,这样用户体验更流畅,但会消耗用户的带宽。两种策略各有优劣,看产品定位。
5.5 更新验证的完整步骤
打包和上传完成后,必须做一次完整的更新验证:
- 在测试机上安装旧版本(比如 1.2.2)。
- 启动应用,观察控制台日志,确认
checkForUpdates被调用。 - 确认客户端请求了
latest.yml,并且解析出了 1.2.3 版本。 - 触发下载,观察进度回调是否正常。
- 下载完成后,确认 SHA512 校验通过。
- 执行
quitAndInstall,确认应用重启后版本号变成 1.2.3。
这个流程走通一次,后续发版就放心了。我建议把验证步骤写成 checklist,每次发版前过一遍,避免遗漏。
6. 常见问题与排查技巧实录
6.1 更新检查返回 404
最常见的原因是 latest.yml 没有上传到正确的位置。客户端请求的地址是 ${publish.url}/latest.yml,如果 publish.url 是 https://update.example.com/releases/latest,那 latest.yml 必须放在 /var/www/update/latest/ 目录下。用浏览器直接访问这个地址,能下载到文件就说明位置对了。
另一个原因是 Nginx 的 alias 配置写错了,导致路径映射不对。检查 Nginx 错误日志,看请求的实际文件路径是什么。
6.2 版本号比较异常
electron-updater 用的是语义化版本比较。如果本地版本是 1.2.3-beta,远程版本是 1.2.3,它会认为远程版本更高。但如果版本号格式不规范,比如 1.2.3.4 或者 v1.2.3,比较逻辑就可能出错。统一用 x.y.z 格式,不要加前缀。
6.3 下载完成后校验失败
SHA512 校验失败通常是文件传输不完整或者被中间层篡改。检查服务器的 Content-Length 是否正确,CDN 有没有做内容压缩导致文件变化。另外,如果用了对象存储,确认文件的 ETag 和实际内容一致。
6.4 macOS 更新后应用无法启动
macOS 的更新包必须是签名的,否则系统会拒绝加载。确认 mac 配置里的签名证书正确,并且做了公证。如果只是内部测试,可以在系统设置里临时允许“任何来源”的应用,但正式发布必须走正规签名流程。
6.5 更新服务器带宽被打满
用户量大了以后,所有客户端同时请求更新包,带宽压力很大。解决方案有两个:一是接入 CDN,把安装包文件缓存到边缘节点;二是做灰度发布,通过不同的 channel 控制更新节奏,比如先让 10% 的用户收到更新,观察没问题再全量。
6.6 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 检查更新返回 404 | latest.yml 路径错误 | 浏览器直接访问更新地址 |
| 提示已是最新但实际有新版 | 版本号格式不规范 | 检查 package.json 里的 version |
| 下载进度卡在 0% | 服务器不支持断点续传 | 检查 Nginx 的 Range 请求支持 |
| 安装后版本号没变 | 安装包没有覆盖旧文件 | 检查 NSIS 的安装目录配置 |
| macOS 提示应用已损坏 | 签名或公证缺失 | 用 codesign 命令验证签名 |
| 更新后白屏 | 渲染进程资源路径错误 | 检查 loadFile 的路径是否用了绝对路径 |
7. 一些实战中攒下来的经验
打包和更新这套东西,文档看一遍觉得不难,真正上手才会发现细节多如牛毛。我把自己踩过的坑和总结的技巧列几条,都是文档里不会写的。
第一,打包前一定要清理 dist 目录。electron-builder 有时候会复用上次的缓存,导致新旧文件混在一起。在打包脚本里加一行 rimraf dist,每次从干净状态开始。
第二,latest.yml 的生成时机要控制好。如果同时打 Windows 和 macOS 包,两个平台的 latest.yml 会互相覆盖。正确的做法是分平台打包,分别上传到不同的目录,或者用 latest.yml 和 latest-mac.yml 区分。
第三,更新服务器的目录权限要设对。Nginx 的运行用户需要有读取权限,否则会返回 403。用 chmod 755 设置目录权限,chmod 644 设置文件权限。
第四,测试更新时用真实的旧版本安装包。不要用开发模式模拟,开发模式和打包后的更新行为有差异。老老实实装一个旧版本,走一遍完整流程。
第五,保留最近几个版本的安装包。用户可能跨版本更新,比如从 1.0.0 直接升到 1.3.0。如果中间版本的增量更新包缺失,electron-updater 会自动回退到全量下载,但保留旧版本文件能让回退更顺畅。
第六,日志要打全。electron-updater 的日志默认输出到控制台,生产环境要重定向到文件。在 autoUpdater.logger 里配置一个文件 logger,出问题的时候有据可查。
这套方案我在几个项目里跑了一年多,从几十个用户到几千个用户,更新成功率一直保持在 99% 以上。核心就是配置要严谨,验证要完整,别偷懒跳过测试步骤。更新链路一旦出问题,用户是直接感知到的,修复成本远高于前期多花时间把配置做对。