首页 / Tauri 2 入门教程 / 命令的参数、返回值与结构体

Tauri 2 入门教程

命令的参数、返回值与结构体

本教程共 48 篇 · 第 21 篇 · 更新于 2026-08-09 · 约 8 分钟阅读

Tauricommandserde结构体IPC

本节目标:学完能用结构体收命令的入参、也能用结构体当返回值,并理解 serde 是怎么在 Rust 和前端之间自动搬运数据的。

上一章你已经知道,命令(command)就是贴在 Rust 函数上的 #[tauri::command],让前端用 invoke 把它叫起来。那函数总得带点参数、带点结果吧?本章就把「传什么、回什么、怎么组织」一次讲透。

多参数命令:前端用 camelCase 传参

命令函数可以像普通 Rust 函数一样接收参数。前端调用时,把这些参数装进一个 JSON 对象里传过去即可。注意一个细节:Tauri 默认希望前端用「小驼峰(camelCase)」的键名,Rust 侧则用「蛇形(snake_case)」的变量名,两者自动对应。

// src-tauri/src/lib.rs
#[tauri::command]
fn greet(first_name: String, last_name: String) -> String {
    format!("你好,{} {}!", last_name, first_name)
}
import { invoke } from '@tauri-apps/api/core';

// 注意键名是 camelCase
await invoke('greet', { firstName: '小明', lastName: '王' });
Note

如果你更习惯在前后端都用蛇形命名(snake_case),给命令加属性即可:#[tauri::command(rename_all = "snake_case")],这样前端传 { first_name: '小明' } 也能对上。

参数类型可以是任意实现了 serde::Deserialize 的类型。常见的基础类型(字符串、数字、布尔)都天然满足,所以日常传参几乎不用操心。也正因为走 JSON,前端传什么类型,Rust 这里就该对应收什么类型:字符串对 String、数字对 i32/f64、布尔对 bool、数组对 Vec、嵌套对象对结构体。类型对不上时,Tauri 会在反序列化阶段直接报错,命令根本不会进到你的函数体里。

Tip

前端调用 invoke 时,参数对象里多出 Rust 不认识的字段并不会报错,Tauri 会安静地忽略它们;但缺了命令必需的字段就会反序列化失败。所以前端传参与 Rust 签名保持一致,是最省心的做法。

用结构体一次性收进入参

当参数一多(比如用户填的一整张表单),一个一个列在函数签名里既难看又难维护。更好的做法是:定义一个结构体,把相关字段打包,然后让命令直接收这个结构体。

// src-tauri/src/lib.rs
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Preferences {
    pub first_name: String,
    pub theme: Theme,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Theme {
    Light,
    Dark,
}

#[tauri::command]
fn save_preferences(preferences: Preferences) {
    println!("收到用户偏好:{:?}", preferences);
}
import { invoke } from '@tauri-apps/api/core';

await invoke('save_preferences', {
  preferences: {
    firstName: '小明',
    theme: 'dark',
  },
});

这里 Preferences 实现了 Deserialize,所以 Tauri 能在收到前端的 JSON 后,自动把它还原成 Rust 结构体。枚举 Theme 同理——前端传小写字符串 "dark",Rust 侧就能匹配到 Theme::Dark

用结构体作为返回值

返回值也一样灵活。你可以直接返回一个结构体,前端拿到的是对应的 JSON 对象,字段名按 #[serde(rename_all = "camelCase")] 的规则自动转成小驼峰。

// src-tauri/src/lib.rs
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ComputeResult {
    message: String,
    other_val: usize,
}

#[tauri::command]
fn compute(number: usize) -> ComputeResult {
    ComputeResult {
        message: "算完了".into(),
        other_val: 42 + number,
    }
}
const res = await invoke<{ message: string; otherVal: number }>('compute', { number: 8 });
console.log(res.message, res.otherVal); // 算完了 50
Tip

返回值的类型只要实现了 serde::Serialize 就行。给返回值加上泛型标注 invoke<MyType>('cmd', ...),前端 TypeScript 就能获得类型提示,写起来更安心。

serde 自动序列化:前后端之间的翻译官

你可能会问:Rust 的结构体、枚举,前端根本不认识,它怎么拿到数据?秘密就在 serde 身上。

serde 是 Rust 生态里负责「序列化(Serialize)」和「反序列化(Deserialize)」的标配库。Tauri 在底层约定:命令的入参必须能反序列化(从前端 JSON 变成 Rust 值),返回值必须能序列化(从 Rust 值变成前端 JSON)。当你给结构体或枚举派生 #[derive(Serialize, Deserialize)] 时,serde 就自动生成了这套转换代码。

所以整条链路是这样的:前端 invoke 传一个 JSON 对象 → Tauri 用 Deserialize 把它变成 Rust 参数 → 你的命令执行 → 返回值用 Serialize 变回 JSON → 前端 await 拿到结果。你写的 Rust 代码里完全看不到 JSON 解析,这正是 serde 替你干的活。

#[serde(rename_all = "camelCase")] 只是最常见的重命名规则之一。如果你想给某个单独字段起个别名,也可以写在字段上:#[serde(rename = "user_name")]。当同一个 Rust 结构体既要当入参又要当出参时,这套重命名规则会同时作用在两侧,保证前端看到的全是小驼峰字段,读起来一致。

Warning

不是所有 Rust 类型都能自动 Serialize/Deserialize。像文件句柄、通道、锁这类「活对象」没法被 JSON 表达。如果你确实要往前端送大块二进制(比如读文件),Tauri 提供了 tauri::ipc::Response 走更高效的通道,不用 JSON 序列化,那是进阶话题,本章先记住「可序列化的数据才能进出命令」。

注册多个命令:统一交给 generate_handler

一个真实项目里命令肯定不止一个。注意:所有命令必须一次性塞进同一个 tauri::generate_handler! 宏里,不能多次调用 invoke_handler(后者只会保留最后一次)。

// src-tauri/src/lib.rs
#[tauri::command]
fn cmd_a() -> String { "A".into() }

#[tauri::command]
fn cmd_b() -> String { "B".into() }

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![cmd_a, cmd_b])
        .run(tauri::generate_context!())
        .expect("启动 Tauri 应用失败");
}

完整最小示例(前后端打通)

下面把上面讲的点拼成一个能直接跑的最小例子。Rust 侧定义一个接收结构体、返回结构体的命令:

// src-tauri/src/lib.rs
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct AddInput { a: i32, b: i32 }

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct AddOutput { sum: i32, doubled: i32 }

#[tauri::command]
fn add(input: AddInput) -> AddOutput {
    let sum = input.a + input.b;
    AddOutput { sum, doubled: sum * 2 }
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![add])
        .run(tauri::generate_context!())
        .expect("启动 Tauri 应用失败");
}

前端这样调用:

import { invoke } from '@tauri-apps/api/core';

const out = await invoke<{ sum: number; doubled: number }>('add', {
  input: { a: 3, b: 4 },
});
console.log(out.sum, out.doubled); // 7 14

小结

到这里你就掌握了命令收发的「形」与「神」:参数和返回值都能是结构体,而 serde 始终在背后做翻译。我们学会了多参数命令的 camelCase / snake_case 自动映射、用结构体打包入参与出参、serde 的序列化与反序列化链路,以及多命令必须统一注册进同一个 generate_handler! 宏。下一章我们来看,当命令执行可能失败时,该怎么把错误安全地送到前端。