桌面端应用的技术选型讨论,通常在「Electron 还是 Tauri」这里就结束了。包体、内存、启动速度,对比表列得很清楚。
然后你开始做发布,才发现真正的成本根本不在这里。
我参与了一个 Tauri 2 桌面应用从零到持续发布的完整过程,覆盖 macOS 双架构与 Windows x64。回头看,那几个月里绝大多数意外都不来自业务代码,而来自打包、签名和分发这条链路------而且几乎每一个都是在启动阶段做的某个决定,到发布阶段才显出代价。
这篇文章讲的就是这些代价。哪些是选型时就注定的、哪些机制被普遍低估、以及一条最容易被误判的:签名。
框架选型省下的包体,转移到了用户机器上
Electron 把 Node 和 Chromium 一并打进安装包,运行环境完全由你掌控,代价是包体上百 MB 起步,且要自己跟进 Chromium 的安全更新。Tauri 2 把渲染交给系统 WebView、系统能力经 Rust 访问,包体小得多。
这笔账的另一半在 Windows 上:Tauri 依赖 WebView2 Runtime,而它不保证存在。 Windows 11 预装,但 Windows 10 的部分版本、精简系统镜像、部分企业受管设备可能没有。
缺失时的症状很有欺骗性:安装过程完全成功,双击后白屏,没有任何报错。用户的结论会是「这个应用坏了」,而不是「我的系统缺组件」。
Tauri 自带 webviewInstallMode 处理这件事,但它的判定口径和失败行为不一定符合你的需要。我们最后选择自己接管------关掉内置模式,在 NSIS 安装器的 pre-install hook 里自己探测:
ini
; 依次检查三处注册表位置,全部为空才判定未安装
; 1. HKLM 的 EdgeUpdate 客户端项
; 2. HKLM 下的 WOW6432Node(32 位视图)
; 3. HKCU(用户级安装)
三处都要查,是因为 WebView2 有系统级和用户级两种安装方式,只查一处会误判。
这个决定连带引出一个更麻烦的结论:安装包必须分裂成两种。
| 完整包 | 更新包 | |
|---|---|---|
| 内置离线 WebView2 安装器 | 是(约 130 MB) | 否 |
| 用途 | 首次安装、无网络环境 | 已装应用的自动更新 |
| 为什么不能合并 | 每次更新都让用户重下 130 MB 运行时 | 首次安装可能装不上 |
两种包由构建参数显式区分,混用的后果不对称:更新包当完整包分发,用户装完白屏;完整包当更新包推送,每次更新多传 130 MB。
如果选 Electron,这一节的成本为零------取而代之的是每个安装包和每次更新都要承担完整 Chromium 的体积。同一份代价,两种付法,没有免费方案。
构建必须发生在目标操作系统上
桌面端产出的是特定操作系统的原生可执行文件,编译、链接、打包都依赖该系统的工具链。所以 macOS 包必须在 macOS 上构建,Windows 包必须在 Windows 上构建。交叉编译在部分场景可行,但一旦涉及签名、安装器和系统 API,基本不成立。
起步阶段这不是问题:一台 Mac 加一台 Windows 笔记本手动发包,最快见效。真正的成本从做持续集成时才开始,而它不在「注册一个 runner」这个动作上,在于把机器变成可重复构建的环境:
- Windows 侧:MSVC 工具链、Windows SDK(
signtool在其中)、Rust 的 MSVC target - macOS 侧:Xcode 命令行工具、对应架构的 Rust target
这些环境是有状态的。SDK 版本变化会改变构建行为,而这类问题往往在几周后以「同样的代码昨天能构建今天不行」的形式出现。
借用别人搭好的构建机,通常行不通
这是新项目最常问的问题。答案通常是不能,原因值得说清:构建机不是通用算力,而是被项目配置绑定的环境。
Runner 的标签可以共享,但每条流水线跑在这台机器上时,都要找到属于自己的那套配置:
| 项目间的差异 | 为什么不能共享 |
|---|---|
| 技术选型 | Tauri 要 Rust 工具链与目标 target,Electron 要 Node 与对应版本的原生模块构建链,两者对机器的要求几乎不重叠 |
| 更新签名密钥 | 私钥与项目一一对应,公钥已编译进各自客户端。共享一把等于让两个产品能互签对方的更新包 |
| 产物上传路径 | 存储桶、路径前缀、更新清单地址都是项目私有的,上传凭据的权限范围本就应该只覆盖自己的路径 |
| 发布流程与门禁 | 版本号规则、发布阶段、强更策略、验收门禁各不相同,写在各自仓库里 |
所以共享一台构建机省下的是硬件,省不掉配置------而配置才是主要成本,且每加一个项目就要在那台机器上多维护一份,并长期跟着它演进。
真正可复用的不是机器,是方法:把工具链安装脚本化,让任何一台干净机器都能重复变成合格的构建机。
托管 runner 能用吗
能,而且是开源项目的主流做法。GitHub Actions 提供托管的 Windows 与 macOS runner,含 Apple silicon 与 Windows arm64 镜像,预装常见工具链,按分钟计费。三个约束决定可行性:
一、代码能否出服务商侧。 源码与构建过程都在对方基础设施上。企业内网仓库通常不允许,这是最常见的一票否决项。
二、签名凭据能否作为 CI 变量注入。 macOS 证书可以(导出为 p12 后注入钥匙串)。插在物理机上的 USB 硬件密钥不行------私钥无法导出,托管 runner 也接不上你的 USB 设备。这一条往下会展开。
三、成本结构,而且桌面端会把它放大。 先分清两种情况:公开仓库用标准 runner 是免费的 ------这正是开源桌面项目普遍用托管 runner 的原因;私有仓库则要算账。私有仓库每月有一份免费额度(GitHub Free 2000 分钟、Pro / Team 3000 分钟),超出后按 runner 单价计费:Linux 每分钟 0.006 美元、Windows 0.010、macOS 0.062。
单看单价还不够,有两个乘数在放大它:
- 系统折算不等价。 免费额度和计费都对不同系统按倍率折算,与单价比例一致------macOS 约为 Linux 的 10 倍、Windows 约 2 倍。也就是说 2000 分钟额度若全用来跑 macOS,实际只够约 200 分钟。
- 桌面端恰好落在最贵的组合上。 它没法只在 Linux 上构建:macOS 包必须在 macOS 上出,而 macOS 正是最贵的一档,且因为不能交叉编译,这一档躲不掉。叠加 Rust 全量编译耗时长、发布又要覆盖多平台多架构(macOS 双架构 + Windows),单次流水线在最贵的 runner 上一跑就是几十分钟。
所以「托管更省事」不等于「托管更便宜」。私有仓库尤其要按 macOS 的真实构建时长乘上倍率去估月成本,别拿 Linux 的直觉套。
结论是可以混用,而且往往应该混用:不涉及凭据、跨平台无关的构建与测试尽量压到便宜的 Linux runner,macOS/Windows 的原生打包再上对应托管 runner,只有签名这一步落到自建机器。
签名和自动更新是同一个问题
这是我认为最被低估的一节。它的反直觉之处在于:签名不是发布流程末尾的一个步骤,而是一个会往回影响架构决策的约束。
macOS:签完名还要公证,而公证有配额
macOS 的链路比「签个名」长一步,而这一步经常被漏掉:
只签名不公证,用户机器仍然会拦截,提示是「已损坏或无法验证开发者」------用户会以为文件下载坏了。
真正影响流程设计的是公证的配额与时延。Apple 的公开限制是每个开发者账号每天 75 次提交。多数请求几分钟内完成,但这不是 SLA,排队时长无法承诺。
这意味着一件事:公证不能放在日常自测路径上。 一天几十次自测就能把配额耗尽,而真正要发版时你被自己堵住了。我们的做法是隔离出一条本地预览通路,不签名、不公证、不生成更新制品,日常验证全走它,配额只留给真正要分发的版本。
Windows:私钥可能被绑死在一支 USB 上
Windows 用 Authenticode 签名,工具是 Windows SDK 里的 signtool。证书由 CA(Certificate Authority,证书颁发机构)签发,代码签名证书分 OV 和 EV(Extended Validation,扩展验证)两档,后者对申请主体的核验更严、对私钥存放的要求也更高。
两个和 macOS 完全不同的地方:
一、证书获取有地域限制。 代码签名证书对申请主体的注册地有要求,并非任何主体都能直接向任意 CA 申请,可能需要通过渠道取得并预留审核周期。这件事应该在项目启动时就去确认,不要等到要发版才发现拿不到证书。
二、EV 证书的私钥必须在硬件里。 2023 年 6 月起,CA/Browser Forum 的基线要求规定代码签名私钥必须存放在 FIPS 140-2 Level 2 或同等硬件中。这就是为什么 EV 证书常以一支 USB 硬件密钥的形式交付------私钥在里面生成,永不导出。
第二条派生出的一整串代价,是我觉得最值得提前知道的部分:
| 后果 | 具体表现 |
|---|---|
| 签名只能在插着密钥的物理机上做 | 云端 runner 再多也没用,它接不上你的 USB 设备 |
| 一次发布要输很多次 PIN | 打包会逐个文件签名:主程序、各 sidecar、安装器辅助程序、NSIS 插件 DLL、卸载程序、安装包本身。一次完整发布约 8 到 11 次签名交互 |
| 机器重启后签名能力就没了 | PIN 缓存清空,需要人工登录解锁。睡眠或 USB 选择性暂停也会让密钥掉线 |
| 长假期间发不出签名包 | 没人去那台机器登录解锁 |
为什么不能把签名机共享给别的项目
这是上面那条约束最容易被误判的推论。直觉上,把签名机注册成共享 runner 只是后台的一次授权。但它触及的是签名权威的边界,不是配置工作量。
假设这个 runner 用 shell 执行器(没有容器隔离),那么任何项目的任务都以这台机器上同一个服务账号的身份运行。三个后果:
凭据同账号可读。 签名相关凭据和上传凭据存放在该账号可读的目录里,文件系统权限分不清「发布任务」和「别人的构建任务」。这是安全边界问题,不是配置瑕疵。
证书可被免 PIN 用于签任意文件。 这条最反直觉。为了让 CI 能无人值守签名,驱动必须配置成单次登录且 PIN 缓存不超时------而这恰恰意味着一旦解锁,任何以该账号运行的进程都能用公司证书签任意文件。这是证书误用风险,比数据泄露更严重。
唯一执行槽被抢占。 签名机通常并发设为 1,别的项目一个长构建就能把发版堵住。
所以正确的扩展方向不是加签名机,而是把构建与签名拆成两层:
构建层不持凭据、无状态、可横向扩容,任何项目任何 runner 都能跑;签名层是单一守卫,凭据只在这一层,产物送进去、签名制品送出来。托管 runner 天然属于构建层------它做不了签名层,恰恰因为它拿不到也不该拿到你的签名凭据。
业界的解法:把私钥从 USB 挪进托管 HSM
根本问题不是权限配置,是私钥绑定在物理硬件上。签名机之所以稀缺、需要协调抢用,就是因为私钥物理存在于那支 USB 里、无法导出。
业界成熟做法是把私钥托管在服务商的 HSM 里,签名通过 API 调用完成,CI 只持有一个可授权、可轮换、可撤销的 API 凭据。
HSM(Hardware Security Module,硬件安全模块)是一种专门用来生成和保管私钥的硬件:私钥在它内部产生、永不以明文离开,外部只能把待签数据送进去、把签名结果取回来。这和 USB 密钥是同一个安全原理------都保证私钥不可导出------区别在形态:USB 密钥是一支必须插在某台机器上的设备,托管 HSM 是服务商机房里的一组设备,通过带鉴权的 API 访问。前者把签名能力绑在一台机器上,后者把它变成一个可授权的网络服务。
私钥进 HSM 后,「多项目共享一台签名机」这个问题本身就消失了:不存在那台机器,凭据变回可按项目和分支授权的 CI 变量。
主流选项和判定:
| 方案 | 适用性 |
|---|---|
| CA 的托管签名服务(如 DigiCert KeyLocker) | 私钥进服务商托管 HSM,签名走 API,无 USB、无 PIN、可并发,不受地域限制。若公司已是该 CA 客户,这是成本最低的一跳 |
| Azure Artifact Signing(原 Trusted Signing) | 其 Public Trust 证书按组织注册地限定,微软文档列出的范围是美、加、欧盟、英、澳新、日、韩、新加坡、瑞士、挪威、以色列。中国大陆主体拿不到。它另有 Private Trust 不受地域限制,但不被公众 Windows 信任,只能用于企业内部信任链 |
| 自建 HSM 加内部签名服务 | 只有当多项目签名规模显著上升、且需要把签名策略与审计完全掌握在内部时才值得投入 |
关于第二条有个常被误解的点:限制条件是签名主体的注册地,不是开发团队或服务器在哪里。 所以集团若在符合条件的地区有真实法律实体,理论上可以用它完成身份验证------但这意味着安装包上向用户显示的发布者就是那个实体,且该实体要真实承担发布责任。这是法务与合规决策,不是绕过地域限制的技术手段。
还有一条与方案选择无关但要在设计流水线时就定下来:无论私钥在 USB 还是 HSM,「发布需要人在场确认」这条原则都应当保留。 托管化解决的是私钥的物理绑定,不是签名授权的随意化。把签名做成任何流水线都能静默调用的一步,等于把证书变成一个谁都能用的接口。
自动更新为什么绑死签名
因为它需要第二套签名,独立于操作系统签名。
Tauri 的 updater 用 minisign 密钥对:私钥留在发布机对更新包签名,公钥编译进客户端,运行时用它验签。这两套签名回答的是不同问题:
- 操作系统签名:这个文件可信吗(未被篡改、发布者可核验)
- 更新签名:这个更新包确实来自我,而不是被劫持的下载
它们互不替代。有操作系统签名但没有更新签名,客户端无法确认自己拉到的更新包是不是原厂的。
这里有一件必须在第一次发布前 就想清楚的事:这对密钥的更换是不可逆的。
公钥已经编译进所有已发出的客户端。你换一对新密钥,老客户端拿着旧公钥,无法验证新私钥签出的更新包------它们会集体失去自动更新能力,只能引导用户手动重新下载。
所以这对密钥必须从第一次发布起就妥善托管、登记保管责任人。它不是一个「以后泄露了再换」的东西。
一条完整的更新链需要三样东西同时就位:签名后的安装包、对应的签名文件、供客户端查询的版本清单(最新版本号、下载地址、文件大小、签名)。每个分发变体各自维护完整一套------不同发布阶段、不同后端环境都要独立维护,跨变体复用会让客户端拉到不匹配的包。
自托管下载页:拿回节奏,接过门禁
应用商店的上架流程繁琐、审核周期不可控,所以很多桌面应用选择自建下载页直接给安装包链接。我们也是这条路。它换来发布节奏的完全可控------这是选它的唯一理由------但商店原本替你做的两件事得自己补上。
第一件是信任背书。 Windows 侧会遇到 SmartScreen 提示,macOS 侧产物未公证会被判定为已损坏,浏览器也可能对可执行文件下载告警。这些是自托管的固有成本,需要在下载页做预期管理和放行指引。
关于 SmartScreen 有个反直觉的事实值得单独说:
EV 签名有效,不等于用户不会看到 SmartScreen 提示。
签名保证的是「文件未被篡改、发布者可核验」,而 SmartScreen 弹窗由信誉决定,信誉按每个文件哈希独立积累------每个新版本都要重新积累。微软早前给 EV 证书的即时豁免已经取消。
排查方法:点开「更多信息」。若发布者显示为你的公司主体,签名是健康的,只是信誉未积累;显示「未知发布者」才是签名有问题。这两种情况的处理方向完全不同,别混。
第二件是发布门禁。 商店会替你拦住有问题的产物,自建下载页得自己建这道闸。我们的规则是:只有公证受理结果为通过、安装包通过票据校验、且发布记录中明确写入已验证状态的产物才上页。
关键在于默认拒绝:证据缺失时视为未知而不是视为通过。这条如果反了,迟早会放过一个没公证的包上线。
启动前值得先定下来的事
如果这篇文章只留一个结论,那就是:桌面端的基础设施成本在选型阶段就已经确定,只是到发布阶段才显现出来。
按时间顺序,这些是我认为值得在写第一行代码前就确认的:
选型阶段------
- 渲染方案,以及它的代价落在包体还是目标机器环境
- 平台与架构矩阵,各平台最低系统版本(每加一项,构建机、签名凭据、验收成本是乘法而非加法)
- 分发形态:商店上架还是自托管下载页
- 公司主体能否申请目标平台的代码签名证书,并预留审核周期
首次发包前------
- macOS 证书就位、公证账号可用,打通签名到公证到票据附加的完整链路
- Windows 证书取得方式确定,明确签名在哪台机器上执行
- 生成并托管自动更新密钥对,明确它不可更换,登记保管责任人
- 设计一条不签名不公证的本地验证通路,避免高频自测消耗公证配额
- 确认目标机器的运行时依赖如何探测与安装,决定是否分裂安装包
接入持续集成时------
- 确认代码能否出内网,据此判定用托管 runner 还是必须自建
- 若签名依赖物理设备,一开始就把构建层与签名层分开
- 建立下载页发布门禁,缺少验证证据时默认拒绝
- 为每个分发变体维护独立的更新清单与制品
这些事里没有一件是「以后再说」能便宜解决的。更新密钥不可更换、证书审核周期不可压缩、签名权威一旦扩散就收不回来------它们的共同点是越晚处理越贵。
你们在桌面端发布上踩过什么坑?特别是签名和自动更新这块,我猜有不少人和我一样是在出问题之后才补的课。
参考与数据源
- Tauri Windows 安装器与 WebView2 配置:v2.tauri.app/distribute/...
- Tauri updater 插件(minisign 密钥机制):v2.tauri.app/plugin/upda...
- Apple 公证文档:developer.apple.com/documentati...
- CA/Browser Forum 代码签名基线要求(私钥硬件存放):cabforum.org/working-gro...
- Azure Artifact Signing 快速入门(含 Public Trust 地域范围):learn.microsoft.com/en-us/azure...
- GitHub Actions 托管 runner 规格:docs.github.com/en/actions/...
- GitHub Actions 计费单价:docs.github.com/en/billing/...
- Microsoft SmartScreen 与应用信誉:learn.microsoft.com/en-us/windo...