命令的参数、返回值与结构体
本教程共 48 篇 · 第 21 篇 · 更新于 2026-08-09 · 约 8 分钟阅读
本节目标:学完能用结构体收命令的入参、也能用结构体当返回值,并理解 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! 宏。下一章我们来看,当命令执行可能失败时,该怎么把错误安全地送到前端。