前端调用 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 入门:错误处理与序列化的语法基础