返回博客

手写 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 调用都会被拦截。

坑:没有 capabilities 文件,IPC 全部静默失败

错误信息 "permission not granted: 'core:window:show'" 只告诉你哪个权限缺失,不会提示你 capabilities 目录根本不存在。第一次调试时很容易去翻 Rust 插件代码,白白浪费时间。

正解:在 src-tauri/capabilities/default.json 声明所有需要的权限
{
  "$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 dev server 的指令,Rollup 看不到它

/* @vite-ignore */ 注释只让 Vite 的开发服务器跳过分析,底层的 Rollup 在执行 vite build仍然会静态分析这个字符串字面量,最终抛出 Failed to resolve import

正解:用字符串拼接让 Rollup 无法静态追踪路径
// 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 字符串。

坑:从 base64 解码视图里「提取」纯密钥内容会破坏签名验证

有人会把 .pub 文件 base64 解码后,只取其中的 RWQ... 裸密钥部分粘进 tauri.conf.json。这会导致更新签名验证失败,更新包被拒绝,且错误信息极不友好。

正解:cat 整个 .pub 文件,原样粘贴
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 对图标有最小尺寸要求。

提示:脚手架阶段用 tauri icon 命令生成完整图标集

准备一张 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 stable

CI 端无需担心,使用 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 路径的场景,都要靠字符串拼接而不是注释。