1. 把 Tauri 的启动链路彻底拆开看
1.1 Tauri 的启动到底在启动些什么
很多人第一次接触 tauri 的启动过程,会本能地拿它跟 Electron 对比。这个对比是对的,但要先搞清楚一个关键差异:Electron 打包出来的是「一个自带的 Chromium 加一份 Node 运行时」,而 Tauri 打包出来的是「一个 Rust 编译的原生可执行文件,加上对系统自带 WebView 的调用」。这个差异几乎决定了后面所有启动、运行、打包环节的技术细节,也是很多人在启动阶段翻车、在打包阶段被体积和兼容性折腾的根本原因。
说人话就是:Electron 是把整个浏览器引擎塞进你的安装包,谁装都得背着这一坨;Tauri 是借宿主系统已经装好的渲染引擎来用,Windows 上借 WebView2(Edge 的内核),macOS 上借 WKWebView(Safari 的内核),Linux 上借 webkit2gtk。所以 Tauri 的启动过程本质上被拆成了两半:一半是 Rust 侧的进程初始化,一半是 WebView 的创建与前端资源加载。这两半谁先谁后、谁来等谁,是整个启动链路的命门。
在你敲下tauri dev或者双击安装完的应用图标之后,实际发生的事情大致是这样一条链路:CLI 或可执行文件读取配置、解析窗口定义、初始化 Rust 运行时、创建 WebView 实例、把前端资源(本地 dev server 地址或打包进二进制的静态文件)塞进 WebView、然后前端 JS 通过 IPC 跟 Rust 侧建立通信通道。任何一环慢了、错了,表现出来都是「窗口没出来」或者「窗口出来了但是白屏」。这两个症状的排查方向完全不同,后面我会分开讲。
我先给一个判断标准:如果你的项目在开发机上能跑,打包之后在别人机器上白屏,那九成是资源路径或者 WebView2 缺失的问题;如果开发阶段tauri dev卡住不动,那基本是前端 dev server 或者 Rust 编译卡在了某个环节。把这两个场景区分清楚,能省掉大量瞎试的时间。
1.2 环境依赖清单:少装一个都跑不起来
Tauri 的环境准备是新手最容易踩坑的地方,因为它的依赖横跨前端和系统层,报错信息还经常指向不明确。我按平台把必须装的东西列一遍,这是基于 Tauri 2.x 的实际情况。
三个通用依赖必须齐活:Rust 工具链(通过 rustup 安装 stable 版本,Tauri 2 建议 1.77 以上)、Node 环境(其实只要能跑你选的前端构建工具即可,用 pnpm、npm、yarn 都行,甚至可以不要 Node 直接写纯静态页)、以及系统 WebView 运行时。
系统 WebView 这块是分平台的:Windows 10/11 自带 WebView2 的情况不统一,Win11 一般预装,Win10 很多机器没有,需要在安装包里带引导;macOS 用系统 WKWebView,不需要额外装;Linux 需要libwebkit2gtk-4.1-dev,注意 Tauri 2 用的是 4.1,Tauri 1 用的是 4.0,这个版本号差一点点就会导致编译报找不到 pkg-config 的错误。
Linux 上还有一串经常被漏掉的依赖,我按 Debian/Ubuntu 系的写法列出来:
sudo apt update sudo apt install -y libwebkit2gtk-4.1-dev \ build-essential \ curl wget file \ libxdo-dev libssl-dev \ libayatana-appindicator3-dev \ librsvg2-dev这里libxdo-dev是给模拟输入用的,librsvg2-dev是图标处理依赖,libayatana-appindicator3-dev是托盘图标依赖。我见过有人只装了 webkit 就开始编译,结果在链接阶段报一堆 undefined reference,然后开始怀疑 Rust 环境坏了,其实只是少了两个 -dev 包。
Windows 上需要 MSVC 构建工具(Visual Studio Build Tools 里勾选「使用 C++ 的桌面开发」),或者退而求其次用 MinGW 的 GNU 工具链。我的建议是直接上 MSVC,因为 WebView2 相关的原生链接在 MSVC 下最省事。macOS 上装 Xcode Command Line Tools 即可,xcode-select --install一条命令解决。
注意:不要在同一台机器上混用多种 Rust target 工具链去编译同一个项目,尤其是 Windows 上 MSVC 和 GNU 混用,产出的二进制在链接 WebView2 加载器时行为不一致,会出现本地能跑、换台机器就崩的情况。
1.3 从命令行到窗口弹出:一次 tauri dev 的完整轨迹
先用脚手架起一个项目,看清楚它生成了什么:
pnpm create tauri-app my-app cd my-app pnpm install pnpm tauri devcreate-tauri-app会问你前端框架、包管理器、UI 模板,选完之后生成的目录结构大致是这样:
my-app/ ├── src/ # 前端源码 ├── index.html ├── package.json ├── vite.config.ts └── src-tauri/ ├── Cargo.toml ├── tauri.conf.json ├── build.rs ├── capabilities/ │ └── default.json ├── icons/ └── src/ ├── main.rs └── lib.rs关键在src-tauri这一层,它是整个原生侧的根。main.rs在 Tauri 2 里被拆得极薄,真正的入口逻辑挪到了lib.rs,这是为了兼容移动端编译(移动端没有 main 函数这个概念):
// src-tauri/src/main.rs #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { app_lib::run(); }// src-tauri/src/lib.rs #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }注意main.rs第一行那句#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")],它的作用是让 release 构建在 Windows 上不弹出那个黑乎乎的控制台窗口,而 debug 构建保留控制台,方便你看到println!的输出。这个细节看似小,但很多人第一次打包完发现多了个控制台窗口,就是不知道去改这里。
tauri dev执行后的完整顺序是:CLI 读取tauri.conf.json,先执行build.beforeDevCommand指定的命令拉起前端 dev server,然后轮询探测build.devUrl是否可访问,探测通过之后再调用cargo run编译 Rust 侧,编译完成后启动二进制,二进制内部通过tauri::generate_context!()宏把编译期注入的配置和前端资源读出来,创建窗口,加载 devUrl。整个过程是串行的,任何一个环节卡住,后面的都不会开始。
1.4 启动时的进程模型:谁在管谁
理解进程模型对排查问题特别有用。Tauri 应用运行时,在操作系统看来就是一个原生进程,它内部持有 WebView 实例。WebView 在 Windows 上会拉起若干msedgewebview2.exe子进程(渲染进程、GPU 进程等),在 macOS 上 WebView 是进程内的,在 Linux 上 webkit2gtk 也会 fork 出 WebKitWebProcess 和 WebKitNetworkProcess。
这意味着两件事。第一,任务管理器里看到的 Tauri 应用内存占用,要把子进程一起算,别只看主进程那几兆然后说 Tauri 比 Electron 省一半——省是真的省,但省的是安装包体积和基础内存占用,渲染复杂度上去了 WebView 一样吃内存。第二,调试的时候如果你发现主进程活着但界面死了,去看 WebView 子进程是不是被系统回收了或者崩了,Windows 上可以用事件查看器查msedgewebview2的异常。
另外 Rust 侧的异步运行时默认用的是 tokio,Tauri 内部把命令调度、事件循环、插件生命周期都挂在上面。所以你在#[tauri::command]里写async fn是天然支持的,不需要自己手动搭 runtime。但要注意:如果在命令里做阻塞式的重 IO(比如同步读大文件、跑一个长时间的循环),会占住执行线程,表现就是界面还能动但所有 IPC 调用都卡住。这种情况用tauri::async_runtime::spawn_blocking包一层。
2. 开发模式运行:前端热更新和 Rust 重编译怎么配合
2.1 tauri.conf.json 里决定启动行为的几个关键字段
配置文件是整个启动过程的总控,Tauri 2 的结构跟 1.x 有较大调整,我把最影响启动行为的字段挑出来讲。很多人是从 1.x 教程迁移过来的,直接照抄distDir和devPath,结果报字段未识别,就是版本对不上。
{ "$schema": "https://schema.tauri.app/config/2", "productName": "my-app", "version": "0.1.0", "identifier": "com.example.myapp", "build": { "beforeDevCommand": "pnpm dev", "devUrl": "http://localhost:1420", "beforeBuildCommand": "pnpm build", "frontendDist": "../dist" }, "app": { "windows": [ { "title": "my-app", "width": 1000, "height": 700, "resizable": true, "visible": false } ], "security": { "csp": null } }, "bundle": { "active": true, "targets": "all", "icon": [ "icons/32x32.png", "icons/128x128.png", "icons/icon.icns", "icons/icon.ico" ] } }几个字段的含义和易错点值得单独说:
| 字段 | 作用 | 常见坑 |
|---|---|---|
build.devUrl | 开发模式下 WebView 加载的地址 | 端口和 Vite 配置不一致,导致一直轮询超时 |
build.frontendDist | 打包时前端产物的相对路径 | 相对的是src-tauri目录,不是项目根,少写一层../就白屏 |
build.beforeDevCommand | 启动前拉起前端服务 | 命令写成前台阻塞模式,会导致 CLI 一直等 |
identifier | 应用唯一标识 | 改动后会影响签名、安装包升级、配置目录路径 |
app.windows[].visible | 窗口初始可见性 | 设成 false 后必须在前端 ready 时手动 show,否则永远看不到窗口 |
identifier这一项我必须强调一下,它是应用的「身份证号」。在 macOS 上它决定签名时的 bundle id,在 Windows 上影响注册表写入路径和单实例判断,在 Linux 上影响配置和数据目录的位置。项目一旦发布,这个值就不要再改了,改了之后老版本用户的自动更新会失败,因为系统认为这是两个不同的应用。
visible: false这个技巧用得好能显著提升体验。做法是窗口先隐藏,前端在合适的时机(比如首屏数据加载完成)通过getCurrentWindow().show()显示出来,这样用户看到的是直接渲染好的界面,而不是一段白屏闪烁。这就是所谓的「无闪烁启动」,实际用起来观感差别挺明显的。
2.2 前端 dev server 与 Rust 侧热重载的实操配置
开发体验的核心是两套热更新各管一摊:前端改动由 Vite(或你用的构建工具)负责,改 JS/CSS 秒级刷新,走的是 WebView 内部的模块热替换;Rust 侧改动由 Tauri CLI 负责,它监听src-tauri目录的文件变化,触发 cargo 增量编译并重启整个进程。
前端这边的重载要能正常работать,Vite 配置得配合上 Tauri 的端口。用create-tauri-app生成的模板里已经写好了:
// vite.config.ts import { defineConfig } from "vite"; export default defineConfig({ clearScreen: false, server: { port: 1420, strictPort: true, watch: { ignored: ["**/src-tauri/**"], }, }, });strictPort: true是关键,端口被占时直接报错而不是自动换端口。因为tauri.conf.json里的devUrl写死了 1420,如果 Vite 偷偷换到 1421,CLI 就会一直轮询 1420,表现就是「命令跑起来了但窗口永远不出现」。这个坑我踩过一次,找了大半天,最后发现是另一个项目占着 1420 端口。
watch.ignored里排除src-tauri也很重要,否则前端 watcher 会去监听 Rust 编译产物,Rust 一重建就触发前端全量刷新,两边互相打架,机器风扇狂转。
Rust 侧的重编译默认是开启的,CLI 会监听src-tauri/src下的.rs文件。但有个细节:tauri.conf.json本身的改动不会自动重启,改完配置要手动 Ctrl+C 重新tauri dev。这一点跟热词里常搜的「运行错误」高度相关,很多人改了窗口尺寸、改了权限配置,发现没生效,以为配置写错了,其实只是没重启。
如果你的 Rust 编译特别慢(依赖多的时候增量编译也要几十秒),可以在Cargo.toml里给 dev profile 做点优化,把依赖的优化级别调低、把自身代码的调试信息保留:
[profile.dev] incremental = true opt-level = 0 [profile.dev.package."*"] opt-level = 2这么配的意思是:第三方依赖编译一次之后基本不变,给它们开opt-level = 2换来更快的运行速度;你自己的代码经常改,保持opt-level = 0换来最快的编译速度。实测下来首次编译会慢一些,但之后的增量编译明显更快,是笔划算的买卖。
2.3 启动卡住时的排查顺序
我把启动阶段的问题整理成一个固定的排查顺序,按这个顺序走基本不会漏。
第一步,看前端 dev server 有没有起来。另开一个终端手动 curl 一下devUrl,能返回 HTML 说明前端没问题,问题在 Rust 侧;返回连接拒绝说明前端根本没起来,去看beforeDevCommand的输出。
第二步,看 Rust 编译有没有报错。cargo 的报错一般很明确,缺依赖、版本冲突、target 不匹配都会指出来。如果卡在Compiling不动,多半是网络拉 crates 慢,配个国内镜像源就能解决。
第三步,看窗口进程起来没。Windows 上任务管理器搜你的productName,macOS 用ps aux | grep找。进程在但窗口不在,就是前端加载失败,右键窗口区域打开开发者工具的快捷键(Windows/Linux 是Ctrl+Shift+I,macOS 是Cmd+Option+I)看控制台报错。
第四步,看 IPC 通道。如果界面出来了但功能不响应,打开控制台看有没有invoke相关的报错。Tauri 2 的权限模型比 1.x 严格得多,命令没在 capabilities 里声明会被直接拒绝,报错信息里有明确的权限缺失提示。
3. tauri build:打包全流程拆解
3.1 打包前的四项准备工作
tauri build不是一条命令就能完事的,前面有几项准备不做,产出的安装包要么装不上,要么装上了有各种奇怪问题。
第一项是图标。Tauri 要求一套多尺寸图标,手动做很麻烦,用 CLI 直接生成:
pnpm tauri icon ./app-icon.png给它一张 1024x1024 的 png,它会在src-tauri/icons下生成 Windows 的.ico、macOS 的.icns、Linux 和通用场景的各个尺寸 png。原始图片必须是正方形且带透明通道,否则生成的图标边缘会有黑边。我见过有人拿一张带白色背景的 jpg 转成 png 直接丢进去,结果 macOS 的 Dock 图标是一坨白色方块,找半天原因。
第二项是版本号。tauri.conf.json里的version字段同时被用于安装包元数据、自动更新比对、Windows 注册表项。改版本号就改这一处,不要去Cargo.toml或者package.json里改,那两个跟安装包版本没有强绑定关系。如果想要三处同步,写个小脚本在构建前跑一下。
第三项是 identifier,前面说过了,发布前定好,之后别动。
第四项是bundle.targets。默认"all"会在当前平台上生成所有它能生成的格式。Windows 上是.msi和.exe,macOS 上是.app和.dmg,Linux 上是.deb、.rpm和.AppImage。想指定的话可以写成数组,比如["nsis"]只要 Windows 的 NSIS 安装包。
3.2 Windows 打包:NSIS 和 WiX 到底选哪个
Windows 上 Tauri 提供两种安装包格式,选择依据是分发场景。
| 对比项 | NSIS(.exe) | WiX(.msi) |
|---|---|---|
| 体积 | 更小,压缩率高 | 相对大一些 |
| 安装模式 | 支持当前用户/全局/两者都选 | 主要面向全局安装 |
| 中文路径 | 兼容性好 | 偶有问题 |
| 企业分发 | 需要额外处理 | 原生支持组策略、SCCM |
| 静默安装 | /S | /quiet |
| 自定义界面 | 需要改 nsi 脚本 | 改 wxs,学习成本高 |
我的实际选择是:面向普通用户的桌面应用一律用 NSIS,因为它体积小、安装模式灵活、中文兼容好;如果是给企业内部批量部署,走 WiX 的 msi 更省事。Tauri 2 里 NSIS 的安装模式改成在配置里写:
{ "bundle": { "windows": { "nsis": { "installMode": "both", "languages": ["SimpChinese", "English"], "displayLanguageSelector": true } } } }installMode三个取值:currentUser只装到当前用户目录,不需要管理员权限;perMachine装到 Program Files,需要提权;both让用户在安装时自己选。给普通用户的产品我建议用currentUser,省掉 UAC 弹窗,用户体验顺畅很多。
还有个必须处理的点:WebView2 运行时的分发策略。配置项是bundle.windows.webviewInstallMode,四个取值各有取舍:
downloadBootstrapper:安装包很小,安装时联网下载。省体积但要求联网,适合大部分场景。embedBootstrapper:把引导程序打进安装包,体积增加约 1.5MB,仍然需要联网下载运行时本体。offlineInstaller:把完整的 WebView2 运行时打进安装包,体积直接涨一百多兆,换来的完全离线可用。skip:不管,假设目标机器已装。适合内网环境自己统一部署过运行时的场景。
Win10 用户基数还很大,我一般用downloadBootstrapper,同时在应用启动时检测 WebView2 是否可用,不可用就引导用户去装。
3.3 macOS 与 Linux 打包的现实约束
macOS 这边有个硬约束必须先说清楚:macOS 的安装包只能在 macOS 机器上打。这跟 Windows 打包可以在 Linux 上借助交叉编译工具链不同,苹果的签名和打包工具链不开放。所以如果你只有一台 Windows 机器,是没法产出 dmg 的,这不是 Tauri 的限制,是苹果生态的限制。
macOS 上如果要同时支持 Intel 和 Apple Silicon,用通用二进制:
pnpm tauri build --target universal-apple-darwin这个命令会把两个架构的产物合并成一个,体积大约翻倍但一份安装包通吃。如果你的用户群集中在新机器上,也可以只打aarch64-apple-darwin,体积能省一半。
Linux 上的情况更碎一些。deb 和 rpm 分别对应 Debian 系和 Red Hat 系,AppImage 是通用格式,双击就能跑不需要安装。AppImage 的构建依赖linuxdeploy,有时候会因为缺少fuse相关组件而失败,这是热词里fpm报错那一类问题的常见来源。遇到打包报错先看是不是缺系统工具,而不是去改项目配置。
关于跨平台打包,我的建议很明确:不要指望在一台机器上打出所有平台的包。Windows 打包最好在 Windows 上做,macOS 必须用 macOS,Linux 建议用对应发行版的容器。真正靠谱的做法是上 CI,用三个平台的 runner 分别构建,这块我在第 5 章会展开。
3.4 体积优化:把安装包从几十兆压到个位数
Tauri 相对 Electron 的体积优势是它的核心卖点,但默认配置下并不一定拿到最优结果。我做过一次对比,同一个应用默认配置打出来 macOS 上是 4.5MB,做了一轮优化之后降到 2.8MB,Windows 的 NSIS 安装包从 3.2MB 降到 2.1MB。具体做法是给 release profile 加配置:
[profile.release] panic = "abort" codegen-units = 1 lto = true opt-level = "s" strip = true逐条解释一下为什么这么写。lto = true开启链接时优化,让编译器跨 crate 做内联和死代码消除,效果最明显但编译时间会明显变长;codegen-units = 1放弃并行代码生成,把整个 crate 当一个单元优化,同样是用编译时间换体积;opt-level = "s"面向体积优化而不是速度,比opt-level = 3小一截,性能损失在大多数界面类应用里感知不到;strip = true去掉符号表,能省掉不少;panic = "abort"让 panic 直接终止进程而不展开栈,省掉展开相关的代码。
注意:
panic = "abort"会让catch_unwind失效,如果你用了某些依赖这个机制的库,会编译不过或者运行异常。遇到这种情况单独把它去掉,其他几项保留。另外 GDB 调试 release 包的时候没有符号表会很痛苦,需要调试时临时关掉strip。
前端侧也有优化空间。Vite 默认的产物已经比较干净,但要确认没有把 sourcemap 一起打进去,build.sourcemap保持默认的 false。如果有大量图标和字体,考虑做子集化和按需加载。另外排查体积构成可以用cargo bloat看 Rust 侧哪些 crate 占了大头,有时候会发现某个功能只用了一个函数却引入了一个巨型依赖。
4. 签名、更新与权限:上线前必须过的坎
4.1 代码签名的实际操作
没签名的应用在现代操作系统上会遭到各种拦截。macOS 上未签名的应用默认打不开,用户得去系统设置里手动放行,这个体验在正式产品里不可接受。Windows 上未签名的安装包会触发 SmartScreen 警告,用户要点「仍要运行」才能装。
macOS 的签名和公证可以在 CI 里通过环境变量自动完成,Tauri CLI 会读取这些变量:
export APPLE_CERTIFICATE="base64编码的p12证书" export APPLE_CERTIFICATE_PASSWORD="证书密码" export APPLE_SIGNING_IDENTITY="Developer ID Application: xxx (TEAMID)" export APPLE_ID="你的开发者账号" export APPLE_PASSWORD="应用专用密码" export APPLE_TEAM_ID="团队ID"这里APPLE_PASSWORD必须是应用专用密码而不是账号登录密码,这一点容易搞混。证书从钥匙串导出成 p12 之后再 base64 编码,编码时不要带换行,否则 CI 里解码会失败。
Windows 的签名相对简单一些,用signtool配合证书文件即可,Tauri 也支持通过配置自动调用。如果没有购买代码签名证书,至少把安装包的元数据填完整(公司名、产品名、版本),可以减少一些杀软误报的概率。
4.2 自动更新的配置与踩坑
Tauri 2 的自动更新由tauri-plugin-updater提供,工作方式是:应用启动时去配置的 endpoint 拉一个 JSON 清单,比对版本号,发现新版就下载更新包、校验签名、然后安装。
签名校验用的是一对密钥,先生成:
pnpm tauri signer generate -w ~/.tauri/myapp.key它会输出公钥和私钥。公钥填进tauri.conf.json的plugins.updater.pubkey,私钥用于给更新包签名,在 CI 里通过TAURI_SIGNING_PRIVATE_KEY和TAURI_SIGNING_PRIVATE_KEY_PASSWORD两个环境变量传给构建过程。私钥绝对不能进代码仓库,这是底线。
配置大致长这样:
{ "bundle": { "createUpdaterArtifacts": true }, "plugins": { "updater": { "endpoints": [ "https://your-server.com/updates/{{target}}/{{arch}}/{{current_version}}" ], "pubkey": "公钥内容" } } }createUpdaterArtifacts打开之后,构建会额外产出.sig签名文件,这个文件必须跟更新包一起传到服务器上,否则客户端校验不过会拒绝更新。
实际踩过的坑有两个。一个是 endpoint 返回的 JSON 格式必须是 Tauri 规定的结构,字段名写错了客户端会静默失败,日志里只有一行级别很低的提示,得开 debug 日志才能看到。另一个是更新包的下载地址在 JSON 里是绝对路径,本地测试时写相对路径是不会被解析的。
4.3 capabilities 权限模型导致的运行报错
Tauri 2 相比 1.x 最大的变化之一就是权限系统。所有从前端调用的能力,包括内置 API 和插件,都必须在src-tauri/capabilities/下的配置文件里显式声明,否则调用会被拒绝。
{ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "默认权限集", "windows": ["main"], "permissions": [ "core:default", "opener:default", "dialog:default", "fs:allow-read-text-file" ] }这个设计的好处是安全边界清晰,坏处是迁移和调试时容易漏。最常见的症状是:开发的某个功能在 1.x 里能跑,升到 2.x 之后报not allowed,去控制台看会明确告诉你是哪个权限缺失,把对应的权限加进去就行。但有些报错不会这么直白,比如文件系统相关的操作,可能会表现成「读到了空内容」而不是抛异常,这时候要去检查fs相关的 scope 配置,不只是加个fs:default就完事,具体路径还需要在 scope 里放行。
5. 常见问题速查与排查思路
5.1 启动与运行阶段的典型问题
我把这些年遇到过的启动类问题整理成一张速查表,症状和原因一一对应:
| 症状 | 大概率原因 | 处理方向 |
|---|---|---|
tauri dev卡住不弹窗 | devUrl 端口被占用或前端没起 | 检查端口,手动 curl devUrl |
| 窗口弹出但白屏 | frontendDist 路径错误或 CSP 拦截 | 打开控制台看报错,检查相对路径 |
| 报找不到 pkg-config | Linux 缺 webkit2gtk-4.1-dev | 装齐系统依赖 |
| 调用命令报 not allowed | capabilities 缺权限声明 | 补权限配置后重启 |
| 界面能显示但点击无反应 | IPC 通道未建立或 JS 报错 | 看控制台,检查 invoke 参数 |
| 中文显示成方框 | 系统缺少字体或前端未指定字族 | 显式指定字体栈 |
第五行那个问题值得多说两句。Tauri 的invoke参数名默认会被转换成 camelCase,Rust 侧如果是 snake_case 的参数,需要加#[tauri::command(rename_all = "snake_case")]或者在配置里改全局约定。不少人遇到的是「参数传过去了但 Rust 收到的是 None」,查半天以为是序列化问题,其实就是命名约定没对齐。
5.2 打包与分发阶段的坑
打包阶段的问题集中在三块:依赖缺失、配置错误、平台差异。
fpm相关的报错基本都出在 Linux 打 deb/rpm 上,检查是不是缺了ruby或者fpm本身没装,Tauri 会尝试自动装但有时候因为权限问题装不上,手动装一下更稳。
Windows 打包报找不到light.exe或者candle.exe,是 WiX 工具链没装好。Tauri 会在首次构建时自动下载,网络不好的话会失败,可以手动装 WiX 3.x 并加到 PATH。
macOS 打包报签名失败,先确认证书在钥匙串里的名称跟APPLE_SIGNING_IDENTITY完全一致,包括括号里的团队 ID。名称对不上会报一个很含糊的错误。
另外有个经常被问到的问题:能不能在 Windows 上打 Linux 包。技术上部分可行(用 Docker 加交叉编译工具链),但 glibc 版本差异会导致打出来的包在老发行版上跑不起来。我的建议是用 Docker 跑对应发行版的容器来构建,比如用ubuntu:20.04的容器打 deb,兼容性覆盖面会好很多。
5.3 用 CI 流水线自动打包
手动在三台机器上打包是低效且容易出错的,正规做法是上 CI。GitHub Actions 是最省事的方案,因为它的 runner 天然覆盖三个平台。核心思路是写一个矩阵任务,每个平台跑自己的构建命令。
关键的处理点有这么几个:Rust 缓存要配上,否则每次构建都要重新编译所有依赖,一次要十几二十分钟;密钥类的东西全部走仓库 secrets,包括 macOS 的证书、Windows 的签名证书、更新用的私钥;构建产物用actions/upload-artifact收集,然后在发布时统一上传。
还有个细节是 macOS 的 runner 架构,GitHub 提供了 arm64 和 x64 两种,要打通用二进制的话需要两个架构都跑一遍或者用支持 universal 的构建方式。另外构建 macOS 应用时要注意 runner 上的 Xcode 版本,版本太低会导致某些系统 API 链接失败。
最后分享一个我自己用的小技巧:在beforeBuildCommand里挂一个脚本,自动把 git 短哈希和构建时间写进前端的一个常量文件,打包出来的应用在「关于」页面显示这两个信息。这样用户反馈问题时,你一眼就能知道对方装的是哪个构建产物,省掉一轮来回确认版本的沟通。这个做法在打包频繁迭代的阶段特别好用。