前端调用 Rust:invoke 与命令系统
命令系统是 Tauri 前后端通信的主干道。前端一次 invoke,背后涉及命令注册、参数反序列化、返回值序列化与错误传递四个环节。本篇把每个环节讲透,并给出自定义类型、错误处理与性能注意点的标准写法。
一、命令的定义与注册
命令就是普通的 Rust 函数,加 #[tauri::command] 宏,并注册到 invoke_handler:
#[tauri::command]
fn double(n: i32) -> i32 {
n * 2
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![double])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
注册表是编译期生成的,拼错命令名会在运行时报错而非编译期报错,这是新手常踩的坑。命令可以返回任何实现了 serde::Serialize 的类型,包括自定义结构体。
二、自定义类型与序列化
实际项目里返回值不会只是数字。用结构体描述更清晰:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct FileInfo {
name: String,
size: u64,
}
#[tauri::command]
fn file_info(path: &str) -> FileInfo {
FileInfo {
name: path.to_string(),
size: 1024,
}
}
前端拿到的就是普通 JS 对象。命名映射遵循 camelCase 约定:Rust 的 size 对应前端的 size,而 created_at 在前端是 createdAt。如果需要自定义映射,用 serde 的 rename 属性,具体写法以 serde 文档为准。结构体定义与 derive 宏的基础知识见 Rust 入门。
前端调用时参数名必须与 Rust 端一一对应,多一个少一个都会失败:
const info = await invoke('file_info', { path: '/tmp/a.txt' })
三、错误处理
命令返回 Result 时,错误会走到前端的 catch。为了让错误信息可读,推荐自定义错误类型并实现 serde::Serialize:
#[derive(Debug, Serialize)]
enum AppError {
NotFound(String),
PermissionDenied,
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppError::NotFound(name) => write!(f, "文件不存在:{}", name),
AppError::PermissionDenied => write!(f, "没有权限"),
}
}
}
#[tauri::command]
fn read_config() -> Result<String, AppError> {
Err(AppError::NotFound("config.toml".into()))
}
前端:
try {
await invoke('read_config')
} catch (e) {
console.error(e) // 序列化后的错误对象
}
| 错误处理方式 | 前端表现 |
|---|---|
| 返回 Result::Err | Promise reject,拿到序列化值 |
| 命令内 panic | Promise reject,消息不可控 |
| 返回 Option | 需自行约定 null 语义 |
永远不要在命令里 panic,错误请走 Result,这是 Tauri 命令的基本规范。
四、参数的高级用法
命令参数除了基本类型,还支持从上下文注入。常用的注入参数类型包括:
use tauri::{AppHandle, State, WebviewWindow};
#[tauri::command]
fn with_context(
app: AppHandle,
window: WebviewWindow,
state: State<'_, MyState>,
) -> String {
format!("{}-{}", app.config().productName, state.counter)
}
这些参数不需要前端传,由框架按类型自动注入。前端调用时依然只传业务参数。State 的更多用法见「全局状态:tauri::State 与共享数据」。
数组和 Vec、HashMap 与 JS 的对象、嵌套结构都能直接传递,前提是两端类型对得上。调试序列化问题时,先在 Rust 端 println! 打印收到的参数,定位是前端没传对还是 Rust 没接对。
五、性能与最佳实践
| 场景 | 建议 |
|---|---|
| 大数据传输 | 避免一次性传超大 JSON,改分页或流式事件 |
| 高频调用 | 考虑用事件批量推送,减少往返 |
| 耗时操作 | 用 async 命令,见「异步命令与后台任务」 |
| 敏感操作 | 命令内做权限校验,别只依赖前端隐藏按钮 |
invoke 是异步的,前端注意不要在循环里无等待地发起大量调用,容易造成 IPC 通道拥塞。用 Promise.all 控制并发是更稳妥的做法。
类型桥接、错误定义这类样板代码,很适合交给 AI 工具生成初稿,参考 Claude Code 指南 的用法;复杂参数结构的调试,正则是好帮手,见 正则 mastery。
六、类型契约与团队协作
命令多了之后,前后端如何就接口达成一致变成协作问题。推荐维护一份命令清单文档,每个命令一行:名称、参数、返回类型、权限要求。这个文档既是前端调用的索引,也是 Capability 声明的依据。
| 协作约定 | 具体做法 |
|---|---|
| 命令命名 | 模块前缀加动词,如 config_read |
| 错误码约定 | 统一文档列出所有枚举值语义 |
| 版本演进 | 只加字段不改旧字段含义 |
| 变更通知 | 命令变更在 PR 模板里强制勾选 |
更进一步可以用代码生成消除手工同步:从 Rust 结构体生成 TS 类型与接口声明,CI 里校验生成产物与提交内容一致。代码生成的初稿很适合交给 AI 工具,参考 Claude Code 指南 的提示词组织方式;生成的类型文件批量重命名、校验时,正则 mastery 的技巧可以直接复用。
命令系统是 Tauri 开发的主旋律:九成业务功能最终都落到一个命令上。把本篇的序列化、错误处理、参数注入三块吃透,后续写命令基本是肌肉记忆。下一章进入事件系统,补上通信的另一半。
到这里命令系统的主干就齐了:注册、参数、返回、错误、注入、性能。日常工作里 80% 的时间花在这些概念之间,剩下的 20% 是插件与状态管理。把本篇的示例亲手敲一遍,大部分典型命令你都能不看文档写出来。
命令系统的本质是把信任从「前端什么都能做」变成「Rust 明确开放什么」。这个视角下命令签名就是接口契约,错误类型是契约的一部分,参数注入是依赖声明。用写后端接口的严谨对待每个命令,应用的可维护性会与其他项目拉开差距。本篇是全书最核心的一章,值得回头反复读。
延伸阅读
- 事件系统:Rust 主动通知前端——另一方向的通信
- 异步命令与后台任务——耗时命令的正确写法
- Rust 入门:错误处理与序列化的语法基础
