Tauri 2 条目式字典:架构 → 配置 → IPC → 权限 → 打包。每条给语义、示例、边界与坑。概念见《Tauri 是什么》,实战见《Tauri 2 架构入门》。
1. 三进程模型与系统 WebView
描述
主进程(Rust 核心)+ WebView 进程(系统 WebView 跑前端)+ 可选辅助进程。不捆绑 Chromium,所以产物体积小(几 MB 级)。
语法和示例
<table>
<thead><tr><th>平台</th><th>WebView 实现</th><th>注意事项</th></tr></thead>
<tbody><tr><td>Windows</td><td>WebView2(Edge 内核,系统预装)</td><td>Win10 以下需安装运行时</td></tr><tr><td>macOS</td><td>WKWebView</td><td>Safari 内核,部分新 API 滞后</td></tr><tr><td>Linux</td><td>WebKitGTK 4.1</td><td>Tauri 2.x 起强制要求 4.1</td></tr><tr><td>移动端</td><td>Android WebView / WKWebView</td><td>Tauri 2 支持,能力子集</td></tr></tbody></table>注意事项
- 跨平台一致性依赖 WebView 版本——Linux 的 WebKitGTK 常落后,先测目标发行版
- 不捆绑 Chromium 省体积但牺牲一致性,与 Electron 是明确取舍
- WebView 进程崩溃会连带窗口白屏,要有兜底提示
- 主进程用 Rust 意味着崩溃隔离更好,但调试栈更陡
- Linux 发行版差异大(Ubuntu 22.04 与 Alpine 的 WebKitGTK 版本不同)
2. tauri.conf.json – 配置结构(v2)
描述
v2 用 app.windows 数组(v1 是单对象 tauri.window);build 段定前端构建;bundle 定打包目标;security 定 CSP。
语法和示例
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "my-app",
"version": "0.1.0",
"identifier": "com.example.app",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"app": {
"windows": [
{ "title": "主窗口", "width": 960, "height": 640,
"resizable": true, "minWidth": 600 }
],
"security": { "csp": "default-src 'self'; img-src 'self' asset: https:" }
},
"bundle": { "active": true, "targets": ["dmg", "msi", "appimage"] }
}注意事项
- identifier 必须全局唯一(用于 macOS bundle id 与更新校验)
- CSP 太松有 XSS 风险,太紧会让前端库失效——从宽松调试再收紧
- frontendDist 是相对 src-tauri 的路径,配错会「白屏无报错」
- v1 的 tauri.window / tauri.allowlist 已不存在,迁移要重写
- 多窗口在 windows 数组里声明,也可运行时动态创建
3. #[tauri::command] – 命令定义
描述
Rust 侧暴露给前端的函数。参数自动反序列化、返回值序列化;async 命令在异步运行时执行,State 注入状态。
语法和示例
#[tauri::command]
fn greet(name: String) -> String { format!("你好, {name}!") }
#[tauri::command]
async fn load(state: tauri::State<'_, AppState>, id: u32)
-> Result<Vec<Item>, String> { // 错误必须是 Serialize 类型
state.fetch(id).await.map_err(|e| e.to_string())
}
// 注册
tauri::Builder::default()
.manage(AppState::default())
.invoke_handler(tauri::generate_handler![greet, load])
.run(tauri::generate_context!())?;注意事项
- 命令名默认是函数名的 snake_case,前端调用也用 snake_case
- 错误类型必须实现 Serialize——常转 String 或自定义错误枚举
- 忘记 generate_handler! 注册的命令前端调用会报「not found」
- async 命令的返回值必须 Send(跨线程)
- 参数名默认 camelCase 映射(Rust 的 user_id 对应前端 userId)
4. invoke – 前端调用命令
描述
@tauri-apps/api/core 的 invoke(name, args) 调用 Rust 命令,返回 Promise。参数键名按 camelCase 映射。
语法和示例
import { invoke } from '@tauri-apps/api/core';
const msg = await invoke<string>('greet', { name: 'X-Rush' });
const items = await invoke<Item[]>('load', { id: 1 });
// 错误捕获
try { await invoke('risky'); }
catch (e) { console.error('命令失败:', e); }
// v1 的路径是 @tauri-apps/api/tauri(v2 改为 /core)注意事项
- invoke 的参数对象键名要与 Rust 参数名 camelCase 对应
- 错误以 reject 的字符串返回(不是 Error 对象),要自己包装
- Windows 上 WebView2 初始化慢,首次 invoke 可能等几百毫秒
- invoke 是异步的,不要在同步渲染函数里直接依赖返回值
- 大量小调用有 IPC 开销——批量接口比循环调用快得多
5. emit / listen – 事件系统
描述
双向事件:Rust 用 app.emit 广播、前端 listen 订阅(返回 unlisten 必须清理)。适合进度、状态推送。
语法和示例
// Rust 侧
#[tauri::command]
async fn start(app: tauri::AppHandle) -> Result<(), String> {
app.emit("progress", serde_json::json!({ "pct": 10 }))
.map_err(|e| e.to_string())?;
Ok(())
}
// 定向:app.emit_to("main", "progress", payload)
// 前端
import { listen, emit } from '@tauri-apps/api/event';
useEffect(() => {
let un: (() => void) | undefined;
listen<{ pct: number }>('progress', (e) => setPct(e.payload.pct))
.then((fn) => { un = fn; });
return () => un?.(); // 必须清理,否则重复监听
}, []);
emit('progress', { pct: 50 }); // 前端也能发注意事项
- listen 未清理会导致每次重新渲染都叠加监听(内存泄漏 + 重复触发)
- 事件 payload 需可序列化(JSON)
- emit 是广播(包括所有窗口),emit_to 定向;大量窗口时注意范围
- 前端 emit 到 Rust 需要 Rust 侧 listen(不是 command)
- 事件无持久化,前端启动晚于事件会丢——关键状态用 command 主动查
6. capabilities – 权限模型(v2 核心)
描述
v2 默认最小权限:每个插件/核心能力都要在 capabilities/*.json 里显式声明。这是与 v1 allowlist 的根本区别。
语法和示例
// src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "主窗口默认能力",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:default",
"fs:allow-read-text-file",
{ "identifier": "fs:scope", "allow": ["$APPDATA/*"] }
]
}注意事项
- 报「not allowed」先查 capabilities,不是查代码逻辑
- permissions 的粒度可到具体函数与路径范围(scope)
- 不同窗口可挂不同能力集(最小权限到窗口级)
- 插件升级可能新增权限项,升级后要重新核对清单
- capabilities 文件在 src-tauri/capabilities/,不是 tauri.conf.json 里
7. 官方插件三步安装
描述
每个官方插件都要三件事:Rust 依赖、npm 包、权限声明。少一步就报错(最常见是漏权限)。
语法和示例
bash
1. 1. Rust 侧
cargo add tauri-plugin-dialog
1. 2. 前端侧
npm i @tauri-apps/plugin-dialog
1. 3. 权限(src-tauri/capabilities/default.json)
1. "permissions": ["dialog:default"]
1. 4. 注册插件(lib.rs)
1. .plugin(tauri_plugin_dialog::init())注意事项
- 漏第 4 步(.plugin() 注册)会报「命令未注册」而不是权限错误
- 插件版本要与 Tauri 主版本匹配(2.x 插件不能配 1.x)
- 移动端插件需额外配置(Android/iOS 的权限清单)
- 自定义插件可不用官方模板,但要自己实现权限检查
- 插件越多启动越慢,按需引入
8. 窗口管理:多窗口与生命周期
描述
Rust 侧 WebviewWindowBuilder 建窗;前端 getCurrentWindow 控制;窗口事件可拦截关闭(转托盘)。
语法和示例
// Rust:动态建窗
tauri::WebviewWindowBuilder::new(
&app, "settings", tauri::WebviewUrl::App("settings".into()))
.title("设置").inner_size(600.0, 400.0).build()?;
// 拦截关闭
.on_window_event(|w, e| {
if let tauri::WindowEvent::CloseRequested { api, .. } = e {
api.prevent_close(); // 转最小化到托盘
}
})
// 前端
import { getCurrentWindow } from '@tauri-apps/api/window';
getCurrentWindow().setTitle('新标题');
getCurrentWindow().close();
getCurrentWindow().listen('tauri://close-requested', () => {});注意事项
- prevent_close 后必须自己处理(最小化/托盘),否则窗口关不掉也退不出
- 窗口 label 唯一,重复创建会报错
- 多窗口共享同一 Rust 状态(State 是全局的),窗口私有状态需自己管理
- 前端 window.close() 会触发 close-requested,被拦截时关不掉属正常
- 窗口位置尺寸要持久化(自行存配置文件,Tauri 不自动记
9. 菜单与系统托盘
描述
macOS 必须显式建主菜单(否则连编辑菜单都没有);托盘用 TrayIconBuilder,配菜单与点击事件。
语法和示例
tauri::Builder::default()
.menu(tauri::menu::Menu::default(&handle)?)
.on_menu_event(|app, e| match e.id().as_ref() {
"quit" => app.exit(0),
"about" => {},
_ => {}
})
.setup(|app| {
let tray = tauri::tray::TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&tray_menu)?
.on_tray_icon_event(|tray, e| { /* 左键显示窗口 */ })
.build(app)?;
Ok(())
})注意事项
- macOS 不建菜单会导致 Cmd+C/V 等系统快捷键失效(用户会以为程序坏了)
- 托盘图标需为模板图(macOS 会反色),否则深色模式下看不见
- 菜单事件用 id 字符串匹配,改动 id 会静默失效
- Linux 的系统托盘依赖 libayatana-appindicator,缺库时不显示
- 托盘与主窗口生命周期独立,全关窗口后进程仍活(靠托盘退出)
10. updater – 自动更新
描述
内置更新器:签名密钥对 + latest.json 清单 + 权限声明。macOS 分发还需 notarize 才能通过校验。
语法和示例
bash
1. 生成签名密钥(公钥进 conf,私钥进 CI 环境变量)
tauri signer generate -w ~/.tauri/myapp.key
1. tauri.conf.json
1. "plugins": { "updater": {
1. "endpoints": ["https://my.site/latest.json"],
1. "pubkey": "..." } }
1. 前端
import { check } from '@tauri-apps/plugin-updater';
const update = await check();
if (update) { await update.downloadAndInstall(); }注意事项
- 私钥泄露等于任何人都能给你的用户推恶意更新——必须放 CI 密钥管理
- latest.json 的 signature 与安装包必须对应,手工改包会校验失败
- macOS 未 notarize 时更新会被 Gatekeeper 拦(开发期常被这个坑住
- 更新会重启应用,要保存用户未提交的数据或提示
- 内网/离线场景需自建 endpoint,且要考虑签名与版本号策略
11. IPC 大数据传输
描述
跨进程传大块数据(文件/图片/二进制):用 tauri::ipc::Response(ArrayBuffer)而非 JSON 序列化。
语法和示例
// Rust
#[tauri::command]
fn read_file() -> tauri::ipc::Response {
tauri::ipc::Response::new(bytes) // 前端收 ArrayBuffer
}
// 前端
const buf: ArrayBuffer = await invoke('read_file');
// 反向:前端传大块
await invoke('upload', { data: new Uint8Array(buf) });注意事项
- JSON 序列化二进制会产生巨大字符串并爆内存——大块必用 Response
- IPC 传输本身仍是拷贝,超大文件建议用分块或本地路径传递
- ArrayBuffer 在 Windows 上走 WebView2 的消息通道,速度受通道限制
- 频繁小 IPC 调用开销明显,合并成批量接口
- 传路径比传内容更好:Rust 侧直接读,前端只拿结果
12. 调试与构建
描述
DevTools 快捷键、Rust 日志、devtools feature、release 构建与产物路径。
语法和示例
bash
npm create tauri-app@latest
npm run tauri dev # 热重载
npm run tauri build # 产物在 src-tauri/target/release/bundle/
1. DevTools:Ctrl+Shift+I(Linux/Win)
1. release 要 devtools 需在 Cargo.toml 加
1. [features] devtools = ["tauri/devtools"]注意事项
- release 默认禁用 DevTools,看不到日志容易误判「没报错」
- Rust 日志用 tauri-plugin-log 或 env_logger,println 在 release 可能不显示
- Linux 构建需装 libwebkit2gtk-4.1-dev 等一整套依赖
- 首次构建要编译全部 Rust 依赖,几分钟到十几分钟属正常
- 跨平台构建要在对应系统或 CI 上做(不能交叉打包 macOS)