在 Tauri 应用中,前端(JavaScript)与后端(Rust)的所有交互都通过 命令(Command) 完成。命令是一个由 Rust 定义、前端通过 IPC 调用的函数。这套机制既保证了类型安全,又天然隔离了 UI 与系统操作。
1. 命令函数定义
在 Rust 侧,使用 #[tauri::command] 宏标记一个普通函数即可将其暴露为可调用的命令,并需要在 main 函数中使用 .invoke_handler(tauri::generate_handler![my_command]) 注册。
最简单的命令:
// src-tauri/src/main.rs 或独立的 commands.rs
#[tauri::command]
fn greet(name: String) -> String {
format!("你好, {}!", name)
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
命令可以是同步函数,也可以是 async 函数,Tauri 会自动在异步运行时中执行它(前提是启用了 async feature)。
异步命令示例:
#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
let resp = reqwest::get(&url).await.map_err(|e| e.to_string())?;
let body = resp.text().await.map_err(|e| e.to_string())?;
Ok(body)
}
注意:需要在
Cargo.toml中启用tauri的async特性,并为异步运行时(如 tokio)配置好。
2. 参数传递
前端通过 invoke 函数(或使用 @tauri-apps/api 包提供的 invoke)调用命令,参数以对象形式传递,键名需与 Rust 函数参数名一致(大小写敏感)。
前端调用:
import { invoke } from '@tauri-apps/api/tauri';
// 调用 greet 命令
invoke('greet', { name: 'Tauri' })
.then((message) => console.log(message)) // 输出:你好, Tauri!
// 调用异步 fetch_data 命令
invoke('fetch_data', { url: 'https://example.com' })
.then((body) => console.log(body))
.catch((err) => console.error(err));
支持数据类型:
Tauri 自动处理序列化(基于 serde),基本类型(String, i32, bool, Vec<u8> 等)开箱即用。如果需要传递复杂结构体,只需为结构体派生 Serialize / Deserialize:
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct Person {
name: String,
age: u8,
}
#[tauri::command]
fn save_person(person: Person) -> Result<(), String> {
println!("接收到: {:?}", person);
// 执行保存逻辑...
Ok(())
}
前端则可以传入一个匹配的 JS 对象:
invoke('save_person', { person: { name: 'Alice', age: 30 } });
参数命名小贴士:
Rust 中的下划线参数会自动转换为 camelCase 供前端使用。例如 Rust 函数 fn my_command(user_id: i32),前端调用时参数名应为 userId。
3. 返回值处理
命令函数的返回值会自动序列化为 JSON 传递给前端,前端收到的是 Promise 的 resolve 值。强烈建议返回 Result<T, E>,其中 E 需实现 Into<tauri::InvokeError>(通常直接使用 String 或 Box<dyn std::error::Error> 即可)。
- 返回
Ok(value)→ 前端then(value)获得数据。 - 返回
Err(message)→ 前端catch(error)捕获,error为字符串形式的错误消息。
自定义错误类型:
#[derive(Debug, thiserror::Error)]
enum MyError {
#[error("无效输入: {0}")]
InvalidInput(String),
}
impl From<MyError> for tauri::InvokeError {
fn from(e: MyError) -> Self {
tauri::InvokeError::from(e.to_string())
}
}
#[tauri::command]
fn validate_number(x: i32) -> Result<i32, MyError> {
if x < 0 {
Err(MyError::InvalidInput("数字不能为负数".into()))
} else {
Ok(x * 2)
}
}
前端:
invoke('validate_number', { x: -5 })
.catch(err => console.error(err)); // 输出: 无效输入: 数字不能为负数
无返回值的情况:
直接返回 () 或 Result<(), _> 即可,前端会得到一个 null 的 resolve 值。
4. 实际开发建议
- 保持命令原子化:一个命令最好只干一件事,通过参数控制行为,避免一个命令承担过多职责。
- 错误信息对用户友好:不要把 Rust 的堆栈信息直接抛给前端,用
thiserror或自定义映射将错误转换为可读描述。 - 大文件或流式数据:对于很大的数据(如文件内容),建议直接在 Rust 侧处理文件路径并返回摘要或处理结果,而非通过命令传递整个文件到前端(有序列化成本)。
- 使用强类型:尽量定义结构体作为参数和返回值,利用 serde 的校验能力,减少运行时类型错误。
通过这套“定义 - 传参 - 返回”的标准流程,Tauri 提供了比传统 Electron IPC 更严谨、更安全的前后端交互方式,同时保留了 Rust 生态的高性能和可靠性。