人人都会AI编程

命令函数定义、参数传递、返回值处理

更新时间:2026-07-11

在 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 中启用 tauriasync 特性,并为异步运行时(如 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>(通常直接使用 StringBox<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 生态的高性能和可靠性。