技术栈: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
- Visual Studio Build Tools 2022(勾选 "使用 C++ 的桌面开发")
- WebView2 Runtime:
- Windows 11:自带,无需安装
- Windows 10:装 Edge 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.toml 和 tauri.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 "你的手机名字"
产物 .ipa 在 src-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::pathAPI 而不是字符串拼接 - 返回的错误信息不泄露内部路径或系统信息
更新安全
- 更新器配置了
pubkey(强制签名验证) - 私钥存在安全位置,不提交到仓库
- 更新 URL 是 HTTPS
依赖安全
- 定期跑
cargo audit(cargo install cargo-audit) - 定期跑
pnpm audit
21. 参考资源
- Tauri v2 官方文档:v2.tauri.app
- Tauri v2 插件列表:v2.tauri.app/plugin/
- Tauri GitHub Actions:github.com/tauri-apps/…
- Tauri Discord:discord.com/invite/taur…
- thiserror:docs.rs/thiserror
- Rust 入门:doc.rust-lang.org/book(官方书,有中文版)
- cargo-audit:github.com/rustsec/rus…