为什么不用 Electron

需求很简单:一个已经在网页上跑得好好的产品,想要一个桌面客户端——能双击打开、能常驻托盘、能自己更新。

第一反应是 Electron,我先把对照实测做了:

对照项 Electron Tauri 2(本项目)
运行时体积 368 MB(实测) 随包不携带浏览器(用系统 WebView2)
首屏时间 3017 ms(实测) 冷启动中位 608 ms
交付包 体积大 7.93 MB(绿色 zip)

差了将近 50 倍。所以我选了 Tauri 2,代价也说清楚:Windows 上依赖 WebView2、Rust 侧调试成本更高、生态比 Electron 小。**这些代价换来的是”用户下载 8 MB 而不是 300 MB”**,对我要交付的场景是划算的。

架构:壳只做两件事

最重要的一条设计决定:这个外壳里没有一行业务逻辑。它只做两件事——进程管理和呈现。

1
2
3
4
5
6
7
8
9
10
11
┌──────────────────────────────────────────┐
│ Tauri 2 壳(Rust) │
│ · 窗口 / 托盘 / 生命周期 │
│ · 启动、监控、优雅停止 sidecar 进程 │
│ · 把 WebView 指向既有前端产物 │
└───────────────┬──────────────────────────┘
│ 同目录(current_exe 父目录定位)
┌───────┴────────┐
│ web/dist │ ← 复用现有前端构建产物,不拷贝、不重写
│ runner (Go 服务) │ ← 二进制作 sidecar,随包分发
└───────────────────┘

配置层面就三行是关键:

1
2
3
4
{
"build": { "frontendDist": "../../web/dist" },
"bundle": { "externalBin": ["binaries/runner"], "targets": ["nsis"] }
}

复用现有产物这件事值得强调:产品的前端已经在迭代了,桌面端不该再造一份。壳加载的就是同一份 web/dist,前端同学改完照常构建,桌面端自动跟上——没有第二份代码需要同步。

验收:四个探针,不靠手感

“能打开、能跑”不是验收。我定了四个可量化的探针,结果写进证据 JSON:

探针 结果 阈值 / 对照
交付包体积 8,319,551 B ≈ 7.93 MB 对照 Electron 368 MB
冷启动(n=5) min 599 / 中位 608 / max 642 ms 阈值 3000 ms ✅
sidecar 可执行性 壳与 sidecar 同目录,current_exe() 父目录定位,只调无害子命令自证通道 ✅
停止 20 次 优雅停止 20/20、硬杀 0、孤儿进程 0、命令行许可泄漏 0,总耗时 55,089 ms 阈值 ≥19 次、孤儿必须 0 ✅

两个容易被忽略的点:

  • 孤儿进程 0:桌面壳最常见的脏问题是”关窗口了,后台进程还在”。我特地统计了停止后是否残留子进程——0 才是通过。
  • 命令行泄漏 0:许可/密钥这类敏感数据绝不能出现在进程命令行里(ps 一抓就有)。所以壳启动 sidecar 时只传无害参数,许可走别的通道。

踩过的六个坑

  1. Windows 优雅停止:直接杀进程会让 sidecar 来不及收尾。正确做法是发 CTRL_C_EVENT(配隐藏控制台),让它自己走完退出流程——这也是”20/20 优雅退出”的前提。
  2. **CSP 要放行 unsafe-eval**:现代前端框架(含某些模板/图表库)在严格 CSP 下会直接白屏。Tauri 默认 CSP 很严,需要显式放行,否则你会在”本地能跑、打包白屏”上耗一晚上。
  3. sidecar 命名必须带 target triple:externalBin 找的是 runner-x86_64-pc-windows-msvc.exe 这种带平台后缀的文件名,名字不对就是”打不出包”。
  4. 第一版不买签名证书:代码签名证书一年 ¥1000–3000,内测阶段不划算。代价是 SmartScreen 会拦——我的做法是发绿色 zip,附一页图文”解除锁定”步骤,把用户操作降到三步以内。
  5. 自动更新要验签:内置 minisign 公钥,更新包签名不对就不装;更新源指向自家接口。这里有个必须在发布前修掉的债:内测期走 http,需要打开 Tauri 的”允许不安全传输”开关——发正式版前必须切 https 并关掉它。
  6. **错误提示不能落”未知错误”**:这是产品级要求。用户看到”未知错误”只会卸载;能自己解释的错误必须写清是什么、为什么、怎么办。

一点体会

桌面端的难点不在 Rust,也不在打包,而在边界:

  • 壳与业务的边界(壳里不放业务逻辑);
  • 进程与进程的边界(谁启动谁、谁负责收尾、敏感信息走哪条路);
  • 内测与发布的边界(哪些是为省事临时开的开关,必须在发布前关掉)。

把这三条边界写清楚,剩下的就是”探针驱动”的重复劳动了——能报出数字,才算交付完成。