手写 Tauri 2 骨架的五个反直觉坑
在自动化流水线或 CI 脚本里集成桌面应用时,npm create tauri-app@latest 无法使用——它是交互式的,要求终端 TTY。于是我选择手写项目骨架:12 个文件,从零组装。Tauri 2 的官方文档只讲交互式脚手架路径,并没有列出手写时会踩的非显然陷阱。这篇文章把我在实战中踩过的五个坑集中记录下来。
现象
- 应用能启动,但托盘点击、窗口 show/hide 等所有 IPC 调用均报 "permission not granted",错误信息只写了权限名,没有提示哪个文件缺失。
- Web 构建(
vite build)警告Failed to resolve import "@tauri-apps/api/event",换成/* @vite-ignore */之后开发模式警告消失,但生产构建依然报错。 - 自动更新签名验证失败,检查签名文件字节数为 0 或更新被服务端拒绝。
坑一:capabilities/default.json 是必须的
Tauri 2 引入了显式的 Capability 声明机制(与 Tauri 1 不同)。如果缺少这个文件,应用可以正常启动,但所有 IPC 调用都会被拦截。
错误信息 "permission not granted: 'core:window:show'" 只告诉你哪个权限缺失,不会提示你 capabilities 目录根本不存在。第一次调试时很容易去翻 Rust 插件代码,白白浪费时间。
{
"$schema": "https://schema.tauri.app/config/2",
"identifier": "default",
"description": "Default capabilities for the desktop app",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:default",
"core:webview:default",
"core:event:default",
"core:app:default",
"deep-link:default",
"updater:default",
"shell:allow-execute"
]
}这是手写骨架最容易漏掉的文件,也是 12 个必须文件里优先级最高的一个。
坑二:@vite-ignore 只对开发服务器有效,Rollup 不认
在一个 monorepo 里,桌面壳和 Web 应用共存。桥接代码 tauri-bridge.ts 在 Tauri WebView 里动态 import @tauri-apps/api/event,而 Web 构建不需要这个依赖。
直觉反应是加 /* @vite-ignore */:
// ❌ 只能压住开发警告,生产构建 Rollup 仍然报错
const { listen } = await import(/* @vite-ignore */ '@tauri-apps/api/event')/* @vite-ignore */ 注释只让 Vite 的开发服务器跳过分析,底层的 Rollup 在执行 vite build 时仍然会静态分析这个字符串字面量,最终抛出 Failed to resolve import。
// src/app/desktop/tauri-bridge.ts
const TAURI_PKG = '@tauri-apps' + '/api'
async function tauriEvent(): Promise<any> {
const path = TAURI_PKG + '/event'
return await import(/* @vite-ignore */ path)
}
export async function subscribeDeeplinks(handler: (url: string) => void) {
const { listen } = await tauriEvent()
return await listen('tauri://deeplink', (e: any) => handler(e.payload))
}Rollup 无法跨变量边界追踪字符串拼接,因此不会尝试解析这个 import。另一个方案是在 vite.config.ts 里显式 external:
build: { rollupOptions: { external: [/^@tauri-apps\//] } }注意:模板字面量 `${TAURI_PKG}/event` 放在动态 import 里在 esbuild 下会报 Expected ; but found $,也不能用。
坑三:ed25519 pubkey 要粘贴完整文件内容
生成密钥对:
npx tauri signer generate --write-keys ~/.tauri/updater.key --ci--ci 会跳过密码提示,生成无密码密钥。.pub 文件是约 120 字节的单行 base64 字符串。
有人会把 .pub 文件 base64 解码后,只取其中的 RWQ... 裸密钥部分粘进 tauri.conf.json。这会导致更新签名验证失败,更新包被拒绝,且错误信息极不友好。
cat ~/.tauri/updater.key.pub | wc -c # 应为约 120 字节,无换行在 tauri.conf.json 里:
{
"plugins": {
"updater": {
"endpoints": ["https://update.example.com/update.json"],
"pubkey": "<整个 .pub 文件的单行内容>"
}
}
}CI 里同时设置(用 --ci 生成的密钥无密码,但密码环境变量仍需设为空字符串,否则部分 Tauri 版本会报 "key password not provided"):
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}坑四:占位图标尺寸不能太小
手写骨架时习惯用最小的 PNG 占位符。但 tauri build 对图标有最小尺寸要求。
准备一张 1024×1024 的 PNG,运行:
npm run tauri icon /path/to/icon-1024.png这会自动生成全平台所需的完整图标集(Apple / Windows / Linux),覆盖 src-tauri/icons/ 目录。
坑五:Homebrew 安装的 Rust 可能版本过低
Tauri 2 要求 Rust 1.77.2+,而 Homebrew 管理的 Rust 更新可能滞后。
brew upgrade rust
# 或者用 rustup 管理
rustup update stableCI 端无需担心,使用 dtolnay/rust-toolchain@stable Action 会自动拉取最新稳定版。
最小文件清单(12 个)
desktop/
├── package.json
├── .gitignore
├── vite.config.ts
├── public-fallback/index.html
└── src-tauri/
├── Cargo.toml
├── build.rs
├── tauri.conf.json
├── capabilities/default.json ← 最容易漏
├── icons/32x32.png
├── icons/128x128.png
└── src/
├── main.rs
└── lib.rs
一句话外卖
Tauri 2 的 capabilities 机制让权限错误沉默为运行时报错而非构建报错,手写骨架时这个文件是第一个要落地的;而
@vite-ignore是 dev-only 魔法,任何需要在生产构建中隐藏动态 import 路径的场景,都要靠字符串拼接而不是注释。