Tauri v2 实操开发文档

6 阅读11分钟

技术栈:TypeScript + React(前端)+ Rust(后端)
目标平台:Windows / macOS / Linux / iOS / Android
深度:从装环境到打包出可分发的安装包


1. Tauri 是什么

Tauri 用系统自带的 WebView 渲染前端(不内置 Chromium),用 Rust 写后端,因此安装包只有 3–8 MB,内存占用极低。

┌─────────────────────────────────────────┐
            你的 Tauri App                
  ┌─────────────────┐  ┌───────────────┐ 
     Rust Core         WebView       
    (tauri crate)  │◄─┤  React UI      
    文件/系统/窗口      TypeScript    
  └─────────────────┘  └───────────────┘ 
                                         
  Win: WebView2 (Edge)                   
  mac: WKWebView (Safari 引擎)            
  Linux: WebKitGTK                       
└─────────────────────────────────────────┘

优势:安装包极小,内存低,安全模型严格(capability 白名单),支持桌面+移动。
劣势:不同平台 WebView 渲染可能有细微差异;需要懂一点 Rust;移动端支持仍在完善中。


2. 环境准备

2.1 所有平台:Rust 工具链

# Linux / macOS(推荐)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"

# Windows:去 https://rustup.rs 下载 rustup-init.exe 运行

验证:

rustc --version    # rustc 1.77+ 即可
cargo --version

更新 Rust(以后定期跑):

rustup update stable

2.2 所有平台:Node.js

  • Node.js ≥ 20 LTS
  • pnpm ≥ 8:npm install -g pnpm

2.3 Windows

  1. Visual Studio Build Tools 2022(勾选 "使用 C++ 的桌面开发")
  2. WebView2 Runtime

2.4 macOS

xcode-select --install

2.5 Linux(Ubuntu / Debian)

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

其他发行版依赖见 v2.tauri.app/start/prere…

2.6 移动端(Tauri v2 新增)

iOS(仅 macOS 可开发):

# 安装 iOS Rust 编译目标
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios

# 需要 Xcode 15+,安装后运行一次确认许可
sudo xcodebuild -license accept

Android:

# 安装 Android Rust 编译目标
rustup target add \
  aarch64-linux-android \
  armv7-linux-androideabi \
  i686-linux-android \
  x86_64-linux-android

# 安装 Android Studio,然后设置环境变量
# macOS/Linux 加入 ~/.zshrc 或 ~/.bashrc
export ANDROID_HOME="$HOME/Library/Android/sdk"   # mac
# export ANDROID_HOME="$HOME/Android/Sdk"         # linux
# Windows Git Bash:
# export ANDROID_HOME="/c/Users/你的名字/AppData/Local/Android/Sdk"

export NDK_HOME="$ANDROID_HOME/ndk/$(ls -1 $ANDROID_HOME/ndk | tail -n1)"
export PATH="$PATH:$ANDROID_HOME/platform-tools"

3. 创建项目

pnpm create tauri-app

交互过程:

✔ Project name · my-tauri-app
✔ Identifier · com.yourcompany.mytauriapp
✔ Choose which language to use for your frontend · TypeScript / JavaScript
✔ Choose your package manager · pnpm
✔ Choose your UI template · React
✔ Choose your UI flavor · TypeScript

进入项目启动:

cd my-tauri-app
pnpm install
pnpm tauri dev

第一次启动:Cargo 会下载并编译 Tauri 的所有 Rust 依赖,约 5–15 分钟(取决于网速和 CPU)。后续增量编译快很多。

3.1 目录结构

my-tauri-app/
├── src/                          # React 前端(Vite 构建)
│   ├── App.tsx
│   ├── main.tsx
│   └── ...
├── src-tauri/                    # Rust 后端
│   ├── src/
│   │   ├── main.rs               # Rust 入口
│   │   └── lib.rs                # 命令和逻辑
│   ├── capabilities/
│   │   └── default.json          # 权限白名单(v2 新增)
│   ├── tauri.conf.json           # 应用配置
│   ├── Cargo.toml                # Rust 依赖声明
│   └── icons/                   # 各平台图标
├── package.json
└── vite.config.ts

4. 核心概念:Command + 权限系统

4.1 Command(Rust 函数暴露给前端)

Tauri 的前后端通信通过 Command:在 Rust 里写函数,加 #[tauri::command] 注解,前端用 invoke() 调用。

前端 (TS)                          后端 (Rust)
  │                                    │
  │  invoke('my_command', {arg: val})  │
  │ ─────────────────────────────────► │  #[tauri::command]
  │                                    │  fn my_command(arg: Type) -> RetType
  │ ◄───────────────────────────────── │  { ... }
  │  返回值 (序列化为 JSON)              │

基本示例:

// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! 你好!", name)
}

// 注册到 Builder
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
// 前端调用
import { invoke } from '@tauri-apps/api/core'

const message = await invoke<string>('greet', { name: 'World' })
console.log(message)  // "Hello, World! 你好!"

4.2 权限系统(v2 重点)

Tauri v2 引入了严格的 capability(能力) 系统。前端默认什么 API 都不能用,必须在 capabilities/default.json 里显式声明。

// src-tauri/capabilities/default.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Default capabilities for desktop",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "fs:allow-read-text-file",
    "fs:allow-write-text-file",
    "fs:allow-mkdir",
    "fs:scope-app-data",
    "dialog:allow-open",
    "dialog:allow-save",
    "shell:allow-open"
  ]
}

常用权限前缀:

前缀功能
core:default基础窗口/通知等,建议始终包含
fs:allow-*文件系统操作
dialog:allow-*系统对话框
shell:allow-open打开外部链接/程序
notification:allow-*系统通知
clipboard-manager:allow-*剪贴板
http:allow-*HTTP 请求(代替 fetch)

5. 完整实战示例:本地 Note 应用

5.1 安装文件系统插件

pnpm tauri add fs
pnpm tauri add dialog

这会自动修改 Cargo.tomltauri.conf.json

5.2 配置权限

// src-tauri/capabilities/default.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "fs:allow-read-text-file",
    "fs:allow-write-text-file",
    "fs:allow-mkdir",
    "fs:allow-exists",
    "fs:scope-app-data",
    "dialog:allow-open",
    "dialog:allow-save"
  ]
}

5.3 Rust 后端

// src-tauri/src/lib.rs
use tauri::{AppHandle, Manager};
use std::fs;
use std::path::PathBuf;

fn notes_dir(app: &AppHandle) -> PathBuf {
    app.path().app_data_dir()
        .expect("no app data dir")
        .join("notes")
}

#[tauri::command]
fn load_note(app: AppHandle, name: String) -> Result<String, String> {
    let dir = notes_dir(&app);
    fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
    
    let file = dir.join(format!("{}.txt", name));
    if file.exists() {
        fs::read_to_string(file).map_err(|e| e.to_string())
    } else {
        Ok(String::new())
    }
}

#[tauri::command]
fn save_note(app: AppHandle, name: String, content: String) -> Result<(), String> {
    let dir = notes_dir(&app);
    fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
    
    let file = dir.join(format!("{}.txt", name));
    fs::write(file, content).map_err(|e| e.to_string())
}

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_fs::init())
        .plugin(tauri_plugin_dialog::init())
        .invoke_handler(tauri::generate_handler![load_note, save_note])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

5.4 React 前端

// src/App.tsx
import { useState, useEffect } from 'react'
import { invoke } from '@tauri-apps/api/core'

const NOTE_NAME = 'default'

export default function App() {
  const [text, setText] = useState('')
  const [saved, setSaved] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    invoke<string>('load_note', { name: NOTE_NAME })
      .then(setText)
      .catch((e) => setError(String(e)))
  }, [])

  const handleSave = async () => {
    try {
      await invoke('save_note', { name: NOTE_NAME, content: text })
      setSaved(true)
      setError(null)
    } catch (e) {
      setError(String(e))
    }
  }

  return (
    <div style={{ padding: 20, fontFamily: 'sans-serif' }}>
      <h2>My Notes {!saved && '●'}</h2>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <textarea
        value={text}
        onChange={(e) => { setText(e.target.value); setSaved(false) }}
        style={{ width: '100%', height: 300, fontSize: 14 }}
      />
      <br />
      <button onClick={handleSave} disabled={saved}>
        {saved ? 'Saved' : 'Save'}
      </button>
    </div>
  )
}

5.5 或者用 JS 插件直接操作文件

如果不想写 Rust Command,可以直接用 @tauri-apps/plugin-fs(需要配好权限):

import { readTextFile, writeTextFile, mkdir, BaseDirectory } from '@tauri-apps/plugin-fs'
import { open, save } from '@tauri-apps/plugin-dialog'

// 读文件(AppData 目录)
const content = await readTextFile('notes/default.txt', {
  baseDir: BaseDirectory.AppData,
})

// 写文件
await writeTextFile('notes/default.txt', text, {
  baseDir: BaseDirectory.AppData,
})

// 文件选择对话框
const path = await open({
  multiple: false,
  filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
})

6. 事件系统

除了 Command(请求-响应),Tauri 还有 Event(发布-订阅),适合后端主动推数据。

Rust 端发送事件:

use tauri::{AppHandle, Emitter};

#[tauri::command]
fn start_task(app: AppHandle) {
    std::thread::spawn(move || {
        for i in 0..=100 {
            std::thread::sleep(std::time::Duration::from_millis(50));
            app.emit("progress", i).unwrap();
        }
    });
}

前端监听:

import { listen } from '@tauri-apps/api/event'

const unlisten = await listen<number>('progress', (event) => {
  console.log('Progress:', event.payload)
})

// 清理
// unlisten()

7. 系统托盘

// src-tauri/src/lib.rs
use tauri::{
    tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
    Manager,
};

pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            let tray = TrayIconBuilder::new()
                .icon(app.default_window_icon().unwrap().clone())
                .tooltip("My Tauri App")
                .build(app)?;
            
            tray.on_tray_icon_event(|tray, event| {
                if let TrayIconEvent::Click {
                    button: MouseButton::Left,
                    button_state: MouseButtonState::Up,
                    ..
                } = event
                {
                    if let Some(win) = tray.app_handle().get_webview_window("main") {
                        let _ = win.show();
                        let _ = win.set_focus();
                    }
                }
            });
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![])
        .run(tauri::generate_context!())
        .expect("error");
}

8. 打包(桌面端)

8.1 tauri.conf.json 配置

{
  "productName": "My App",
  "version": "1.0.0",
  "identifier": "com.yourcompany.myapp",
  "build": {
    "beforeBuildCommand": "pnpm build",
    "devUrl": "http://localhost:1420",
    "frontendDist": "../dist"
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": [
      "icons/32x32.png",
      "icons/128x128.png",
      "icons/128x128@2x.png",
      "icons/icon.icns",
      "icons/icon.ico"
    ]
  },
  "app": {
    "windows": [
      {
        "label": "main",
        "title": "My App",
        "width": 900,
        "height": 600,
        "resizable": true
      }
    ]
  }
}

8.2 图标

脚手架已在 src-tauri/icons/ 生成了默认图标。替换成自己的:

# 安装 tauri-cli 的图标生成工具
pnpm tauri icon 你的图标.png
# 会自动生成所有尺寸,放到 src-tauri/icons/

源图:建议 1024×1024 PNG,透明背景。

8.3 打包命令

pnpm tauri build

第一次也会较慢(编译 release 模式的 Rust)。后续有缓存会快很多。

8.4 产物位置

src-tauri/target/release/bundle/
├── msi/
│   └── My App_1.0.0_x64_en-US.msi         # Windows MSI 安装包
├── nsis/
│   └── My App_1.0.0_x64-setup.exe          # Windows NSIS 安装包(推荐)
├── dmg/
│   └── My App_1.0.0_aarch64.dmg            # macOS 磁盘镜像
├── macos/
│   └── My App.app                           # macOS app bundle
├── appimage/
│   └── my-app_1.0.0_amd64.AppImage         # Linux 通用包
└── deb/
    └── my-app_1.0.0_amd64.deb              # Linux Debian 包

对比 Electron:同样功能的应用,Tauri 的 setup.exe 只有 3–5 MB,Electron 的是 80–120 MB。

8.5 跨架构打包

# Apple Silicon Mac 上打 Intel 版
rustup target add x86_64-apple-darwin
pnpm tauri build --target x86_64-apple-darwin

# 打 Universal Binary(Intel + ARM,体积是两倍)
pnpm tauri build --target universal-apple-darwin

# Windows arm64
rustup target add aarch64-pc-windows-msvc
pnpm tauri build --target aarch64-pc-windows-msvc

8.6 代码签名

Windows(避免 SmartScreen 警告):

在环境变量中设置:

TAURI_SIGNING_PRIVATE_KEY=你的私钥
TAURI_SIGNING_PRIVATE_KEY_PASSWORD=密码

或在 tauri.conf.json 配置 Windows 签名(使用 .pfx 证书):

{
  "bundle": {
    "windows": {
      "certificateThumbprint": "证书指纹",
      "digestAlgorithm": "sha256",
      "timestampUrl": "http://timestamp.digicert.com"
    }
  }
}

macOS(避免 Gatekeeper 拦截):

需要 Apple Developer 账号。在环境变量中设置:

APPLE_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAM_ID)"
APPLE_ID="你的Apple ID邮箱"
APPLE_PASSWORD="应用专用密码"
APPLE_TEAM_ID="你的 Team ID"

Tauri 的 tauri build 检测到这些环境变量会自动签名和公证。


9. 打包(移动端)

9.1 初始化移动端支持

# 初始化(项目里只需执行一次)
pnpm tauri ios init
pnpm tauri android init

这会生成 src-tauri/gen/apple/src-tauri/gen/android/ 目录。

9.2 iOS

# 开发(需要 macOS + Xcode + 模拟器或真机)
pnpm tauri ios dev

# 在特定模拟器运行
pnpm tauri ios dev --device "iPhone 15"

# 打包(需要 Apple Developer 账号)
pnpm tauri ios build

iOS 开发调试:

# 列出可用设备
pnpm tauri ios device list

# 真机调试(连上 Mac,需要信任)
pnpm tauri ios dev --device "你的手机名字"

产物 .ipasrc-tauri/gen/apple/build/ 下。上架 App Store 需要在 Xcode 里做 Archive → Distribute。

9.3 Android

# 开发(启动 Android 模拟器或连接真机)
pnpm tauri android dev

# 列出可用设备
pnpm tauri android device list

# 打包
pnpm tauri android build                     # 出 .apk 和 .aab
pnpm tauri android build --apk               # 只出 .apk
pnpm tauri android build --aab               # 只出 .aab(上传 Google Play)

Android 签名(正式发布):

# 生成 keystore(只做一次,妥善保管!)
keytool -genkey -v \
  -keystore my-release-key.keystore \
  -alias my-key-alias \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000

# 设置环境变量
export TAURI_ANDROID_KEYSTORE="$(pwd)/my-release-key.keystore"
export TAURI_ANDROID_KEYSTORE_PASSWORD="你的密码"
export TAURI_ANDROID_KEY_ALIAS="my-key-alias"
export TAURI_ANDROID_KEY_PASSWORD="你的密码"

pnpm tauri android build

产物在 src-tauri/gen/android/app/build/outputs/ 下。


10. 加速开发:sccache 编译缓存

Rust 编译慢的主要解法:

cargo install sccache

# macOS/Linux
export RUSTC_WRAPPER=sccache

# Windows
setx RUSTC_WRAPPER sccache

加了缓存后,再次编译相同代码会瞬间完成。


11. 常见问题排查

pnpm tauri dev 第一次太慢

正常。Rust 第一次编译 Tauri 所有依赖约 5–15 分钟。之后增量编译通常 5–30 秒。装了 sccache 后切换分支也快。

Windows 上 WebView2 报错

症状:Error: WebView2 is not installed

解决:让用户去安装 Microsoft Edge WebView2 Runtime,或在打包配置里设置自动下载引导:

// tauri.conf.json
{
  "bundle": {
    "windows": {
      "webviewInstallMode": {
        "type": "downloadBootstrapper"
      }
    }
  }
}

invoke 返回 Rust 错误

Rust 的 Result::Err 会被序列化成字符串抛到前端的 Promise reject

// 前端要 try/catch
try {
  await invoke('my_command', { arg: value })
} catch (e) {
  // e 是字符串,来自 Rust 的 Err(string)
  console.error('Rust error:', e)
}

macOS 打包报 "ld: library not found"

通常是缺少 Xcode Command Line Tools:

xcode-select --install
sudo xcode-select -r   # 重置路径

Linux 不同发行版兼容

WebKitGTK 版本在不同发行版差异较大。如果要发行,尽量打 AppImage(包含大部分依赖),或者在目标发行版的 Docker 里打包。

前端 import 的 @tauri-apps/api/core 找不到

确认 @tauri-apps/api 已安装:

pnpm add @tauri-apps/api

v2 的导入路径和 v1 不同:

// v2(正确)
import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event'

// v1(旧,不要用)
import { invoke } from '@tauri-apps/api/tauri'

12. Rust 快速入门(你只需要知道这些)

不需要会写 Rust,但看懂 Command 要知道几个概念:

// 函数签名
#[tauri::command]
fn my_command(
    app: AppHandle,          // Tauri 运行时句柄,访问路径/窗口等
    name: String,            // 前端传来的参数(自动从 JSON 反序列化)
    count: u32,              // 无符号 32 位整数
) -> Result<String, String>  // Ok(值) = 成功,Err(消息) = 失败
{
    if name.is_empty() {
        return Err("name cannot be empty".to_string());
    }
    Ok(format!("Hello, {}! Count: {}", name, count))
}

Rust 错误处理套路:

// 用 ? 传播错误(类似 try 的语法糖)
fn do_file_stuff(path: &str) -> Result<String, String> {
    let content = std::fs::read_to_string(path)
        .map_err(|e| e.to_string())?;   // 出错就提前返回 Err
    Ok(content.to_uppercase())
}

这就够写大部分 Command 了。需要更复杂的逻辑时查 Rust 文档即可。


13. 自动更新

Tauri 内置了更新器,不需要额外的库。

13.1 配置更新服务端

以 GitHub Releases 为例:

// src-tauri/tauri.conf.json
{
  "plugins": {
    "updater": {
      "pubkey": "YOUR_PUBLIC_KEY",
      "endpoints": [
        "https://github.com/your-org/your-repo/releases/latest/download/latest.json"
      ],
      "dialog": false,
      "windows": {
        "installMode": "passive"
      }
    }
  }
}

pubkey 用于验证更新包签名。生成密钥对:

pnpm tauri signer generate -w ~/.tauri/myapp.key
# 输出公钥,粘贴到 tauri.conf.json 的 pubkey 字段
# 私钥路径:~/.tauri/myapp.key(保密,用于构建时签名)

13.2 安装更新插件

pnpm tauri add updater

13.3 前端更新 UI

// src/updater.ts
import { check, Update } from '@tauri-apps/plugin-updater'
import { relaunch } from '@tauri-apps/plugin-process'

export async function checkForUpdates(): Promise<Update | null> {
  try {
    return await check()
  } catch (e) {
    console.error('Update check failed:', e)
    return null
  }
}

export async function downloadAndInstall(
  update: Update,
  onProgress: (downloaded: number, total: number) => void
) {
  let downloaded = 0

  await update.downloadAndInstall((event) => {
    if (event.event === 'Started') {
      downloaded = 0
    } else if (event.event === 'Progress') {
      downloaded += event.data.chunkLength
      onProgress(downloaded, event.data.contentLength ?? 0)
    } else if (event.event === 'Finished') {
      console.log('Download finished')
    }
  })

  await relaunch()
}

React 组件:

import { useEffect, useState } from 'react'
import { type Update } from '@tauri-apps/plugin-updater'
import { checkForUpdates, downloadAndInstall } from './updater'

export function UpdateBanner() {
  const [update, setUpdate] = useState<Update | null>(null)
  const [progress, setProgress] = useState(0)
  const [installing, setInstalling] = useState(false)

  useEffect(() => {
    // 启动 10 秒后检查
    const t = setTimeout(() => {
      checkForUpdates().then(setUpdate)
    }, 10_000)
    return () => clearTimeout(t)
  }, [])

  if (!update) return null

  return (
    <div style={{ padding: 12, background: '#e8f4fd', borderRadius: 8 }}>
      <p>发现新版本 {update.version},是否更新?</p>
      {installing && <progress value={progress} max={100} />}
      <button
        onClick={async () => {
          setInstalling(true)
          await downloadAndInstall(update, (dl, total) => {
            if (total > 0) setProgress(Math.round((dl / total) * 100))
          })
        }}
        disabled={installing}
      >
        {installing ? '更新中...' : '立即更新'}
      </button>
    </div>
  )
}

13.4 发布时签名

# 设置私钥环境变量,然后正常构建
export TAURI_SIGNING_PRIVATE_KEY=$(cat ~/.tauri/myapp.key)
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD=你的密码

pnpm tauri build

构建完会在产物旁边生成 .sig 签名文件,上传到 GitHub Release 时要把签名文件一起上传。


14. Rust 侧状态管理

Tauri 应用里,多个 Command 之间共享数据用 State 机制。

14.1 定义和注册共享状态

// src-tauri/src/lib.rs
use std::sync::Mutex;
use tauri::State;

// 你的应用状态
struct AppState {
    counter: Mutex<u32>,
    user_name: Mutex<Option<String>>,
}

// 在 Command 里访问
#[tauri::command]
fn increment(state: State<AppState>) -> u32 {
    let mut counter = state.counter.lock().unwrap();
    *counter += 1;
    *counter
}

#[tauri::command]
fn set_user(state: State<AppState>, name: String) {
    let mut user = state.user_name.lock().unwrap();
    *user = Some(name);
}

#[tauri::command]
fn get_user(state: State<AppState>) -> Option<String> {
    state.user_name.lock().unwrap().clone()
}

pub fn run() {
    tauri::Builder::default()
        .manage(AppState {
            counter: Mutex::new(0),
            user_name: Mutex::new(None),
        })
        .invoke_handler(tauri::generate_handler![increment, set_user, get_user])
        .run(tauri::generate_context!())
        .expect("error");
}

14.2 更好的错误类型(thiserror)

生产代码别用裸 String 做错误类型,用 thiserror

# src-tauri/Cargo.toml
[dependencies]
thiserror = "1"
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Config parse error: {0}")]
    ConfigParse(#[from] serde_json::Error),
    #[error("Permission denied: {0}")]
    PermissionDenied(String),
}

// 实现 serde::Serialize 让 Tauri 能序列化给前端
impl serde::Serialize for AppError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where S: serde::Serializer {
        serializer.serialize_str(&self.to_string())
    }
}

// Command 现在返回具体错误类型
#[tauri::command]
fn read_config(path: String) -> Result<String, AppError> {
    let content = std::fs::read_to_string(&path)?;   // Io 错误自动转换
    Ok(content)
}

15. 环境变量与配置管理

15.1 区分环境

// Rust 侧判断是否是 debug 构建
#[cfg(debug_assertions)]
fn is_dev() -> bool { true }

#[cfg(not(debug_assertions))]
fn is_dev() -> bool { false }

前端侧:

import { isDev } from '@tauri-apps/api/core'   // 不存在,用下面方法

// Vite 注入
const isProd = import.meta.env.PROD
const isDevelopment = import.meta.env.DEV

15.2 Vite 环境变量

# .env.development
VITE_API_URL=http://localhost:3000

# .env.production
VITE_API_URL=https://api.yourapp.com

渲染进程:import.meta.env.VITE_API_URL

15.3 用户配置持久化(store 插件)

pnpm tauri add store
// 前端:像 localStorage 一样,但支持复杂对象和自动持久化
import { Store } from '@tauri-apps/plugin-store'

const store = await Store.load('config.json')

await store.set('theme', 'dark')
const theme = await store.get<string>('theme')
await store.save()   // 手动持久化(或配置自动保存)
// capabilities/default.json 加
"store:allow-load",
"store:allow-get",
"store:allow-set",
"store:allow-save"

16. 日志与错误监控

16.1 日志插件(tauri-plugin-log)

pnpm tauri add log

Cargo.toml 会自动加依赖。配置 Rust 侧:

// src-tauri/src/lib.rs
pub fn run() {
    tauri::Builder::default()
        .plugin(
            tauri_plugin_log::Builder::new()
                .level(log::LevelFilter::Info)
                .build()
        )
        .run(tauri::generate_context!())
        .expect("error");
}

前端侧:

import { info, warn, error, debug } from '@tauri-apps/plugin-log'

info('App started')
warn('Config not found, using defaults')
error('Failed to connect', { url: 'ws://...' })

Rust 侧:

use log::{info, warn, error};

info!("Server started on port {}", port);
error!("Database connection failed: {}", e);

日志文件位置:

  • Windows: C:\Users\<用户>\AppData\Roaming\<app-id>\logs\
  • macOS: ~/Library/Logs/<app-id>/
  • Linux: ~/.local/share/<app-id>/logs/

16.2 Sentry 错误上报

前端侧用标准的 @sentry/browser

pnpm add @sentry/browser
// src/main.tsx
import * as Sentry from '@sentry/browser'
import { invoke } from '@tauri-apps/api/core'

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  environment: import.meta.env.PROD ? 'production' : 'development',
  enabled: import.meta.env.PROD,
})

Rust 侧崩溃上报(通过 sentry crate):

# Cargo.toml
[dependencies]
sentry = { version = "0.34", default-features = false, features = ["backtrace", "contexts", "panic"] }
let _guard = sentry::init(("your-dsn", sentry::ClientOptions {
    release: sentry::release_name!(),
    ..Default::default()
}));

17. CI/CD(GitHub Actions)

17.1 多平台构建并发布

# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'

permissions:
  contents: write

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        include:
          - platform: windows-latest
            args: '--target x86_64-pc-windows-msvc'
          - platform: macos-latest
            args: '--target aarch64-apple-darwin'
          - platform: macos-latest
            args: '--target x86_64-apple-darwin'
          - platform: ubuntu-22.04
            args: ''

    runs-on: ${{ matrix.platform }}

    steps:
      - uses: actions/checkout@v4

      - name: Install Linux deps
        if: matrix.platform == 'ubuntu-22.04'
        run: |
          sudo apt update
          sudo apt install -y libwebkit2gtk-4.1-dev libappindicator3-dev \
            librsvg2-dev patchelf

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - name: Install Rust stable
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}

      - name: Rust cache
        uses: swatinem/rust-cache@v2
        with:
          workspaces: './src-tauri -> target'

      - name: Install frontend deps
        run: pnpm install --frozen-lockfile

      - name: Build and publish
        uses: tauri-apps/tauri-action@v0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
          # macOS 签名
          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
        with:
          tagName: v__VERSION__
          releaseName: 'v__VERSION__'
          releaseBody: 'See the assets to download and install this version.'
          releaseDraft: true
          args: ${{ matrix.args }}

17.2 自动生成 latest.json(更新元数据)

tauri-apps/tauri-action 会自动生成 latest.json 并附到 Release。格式:

{
  "version": "1.0.1",
  "notes": "Bug fixes",
  "pub_date": "2025-01-01T00:00:00Z",
  "platforms": {
    "windows-x86_64": {
      "url": "https://github.com/.../MyApp_1.0.1_x64-setup.exe",
      "signature": "..."
    },
    "darwin-aarch64": {
      "url": "https://github.com/.../MyApp_1.0.1_aarch64.dmg",
      "signature": "..."
    }
  }
}

18. 深度窗口配置

18.1 透明窗口 / 无边框窗口

// tauri.conf.json
{
  "app": {
    "windows": [
      {
        "label": "main",
        "decorations": false,
        "transparent": true,
        "shadow": true
      }
    ]
  }
}
// 自定义拖拽区域(无边框时需要)
// 前端 HTML 元素加 data-tauri-drag-region 属性
// React
<div
  data-tauri-drag-region
  style={{ height: 32, background: 'rgba(0,0,0,0.1)', cursor: 'default' }}
>
  My Custom Titlebar
</div>

18.2 多窗口

use tauri::{AppHandle, Manager, WebviewWindowBuilder};

#[tauri::command]
async fn open_settings(app: AppHandle) -> Result<(), String> {
    // 已存在则聚焦
    if let Some(win) = app.get_webview_window("settings") {
        win.set_focus().map_err(|e| e.to_string())?;
        return Ok(());
    }

    WebviewWindowBuilder::new(&app, "settings", tauri::WebviewUrl::App("settings".into()))
        .title("Settings")
        .inner_size(600.0, 500.0)
        .resizable(false)
        .build()
        .map_err(|e| e.to_string())?;

    Ok(())
}

19. 性能优化

19.1 懒加载前端

Vite + React 天然支持代码分割:

import { lazy, Suspense } from 'react'

const Settings = lazy(() => import('./pages/Settings'))
const About = lazy(() => import('./pages/About'))

19.2 Rust 侧并发(tokio)

# Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
use tauri::async_runtime;

#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
    // Tauri Command 天然支持 async
    let resp = reqwest::get(&url)
        .await
        .map_err(|e| e.to_string())?;
    resp.text().await.map_err(|e| e.to_string())
}

CPU 密集任务放到线程池,不阻塞 async 运行时:

#[tauri::command]
async fn compute_hash(data: String) -> String {
    // spawn_blocking 把同步重计算移到线程池
    tauri::async_runtime::spawn_blocking(move || {
        use sha2::{Sha256, Digest};
        let hash = Sha256::digest(data.as_bytes());
        format!("{:x}", hash)
    })
    .await
    .unwrap()
}

19.3 减小前端包体积

// vite.config.ts
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
        },
      },
    },
    // 生产环境移除 console
    minify: 'terser',
    terserOptions: {
      compress: { drop_console: true },
    },
  },
}

20. 完整安全检查清单(生产版)

权限系统

  • capabilities/default.json 只声明实际需要的权限
  • 不用 fs:allow-*(通配),用具体的 fs:allow-read-text-file
  • 配置 fs:scope-* 限制可访问的目录范围

Command 安全

  • 所有 Command 参数都做校验(类型 + 业务规则)
  • 文件路径参数用 tauri::path API 而不是字符串拼接
  • 返回的错误信息不泄露内部路径或系统信息

更新安全

  • 更新器配置了 pubkey(强制签名验证)
  • 私钥存在安全位置,不提交到仓库
  • 更新 URL 是 HTTPS

依赖安全

  • 定期跑 cargo auditcargo install cargo-audit
  • 定期跑 pnpm audit

21. 参考资源