首页 / Tauri 2 入门教程 / Rust 命令(command)入门

Tauri 2 入门教程

Rust 命令(command)入门

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

TauriRustcommandIPC

本节目标:学完能自己写一个 Rust 命令,用 invoke_handler 注册,并在前端用 invoke 调通它,理解参数传递、返回值和错误处理的基本规则。

命令(command)是 Tauri 里「前端调用 Rust 函数」的标准通道,也是整个框架最有价值的能力之一。你可以把它理解成一座桥:桥这头是网页里的 JavaScript/TypeScript,桥那头是 Rust 编译进安装包的原生代码。通过命令,前端能安全地用上 Rust 的文件读写、计算、系统调用等能力。本章只讲机制和一个最小可运行示例,不带着做一个完整产品。所有写法均对照 Tauri 2.x 官方文档。

命令究竟是什么

在 Tauri 里,一个「命令」就是一个普通的 Rust 函数,但被 #[tauri::command] 这个属性宏(attribute macro)标记过。宏的作用是在编译期帮你生成「把这个函数暴露给前端」所需的一堆胶水代码——你不用手写序列化和路由,标记一下就行。

最小的一个命令,连参数都没有:

// src-tauri/src/lib.rs
#[tauri::command]
fn my_custom_command() {
    println!("我是从 JavaScript 被调用的!");
}

光写函数还不够,Tauri 不知道该把哪些命令开放出去。你必须在应用的 run() 函数里,用 .invoke_handler(tauri::generate_handler![...]) 把它登记上:

// src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![my_custom_command])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

generate_handler! 是一个宏,方括号里填你要注册的所有命令名。注意:只能调用一次 invoke_handler,要注册多个命令就全写进同一个方括号,用逗号分隔,不能分成多个 .invoke_handler(...)

Note

命令名必须全局唯一。另外,命令如果直接写在 lib.rs 里,不要加 pub 关键字——宏生成的胶水代码会和 pub 冲突,编译会报 __cmd__xxx defined multiple times 之类的错。把命令拆到独立模块时再标 pub(见后文)。

前端如何调用命令

Rust 端登记好之后,前端就能用 invoke 调它。在 TypeScript 里这样写:

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

// 调用无参命令
invoke('my_custom_command');

invoke 的第一个参数是命令名(字符串),返回的是一个 Promise。所以即便命令没有返回值,用 await.then() 来等待它执行完也是最稳妥的写法。

传递参数:默认用 camelCase

命令几乎总需要参数。Rust 端把参数写成函数入参即可:

#[tauri::command]
fn my_custom_command(invoke_message: String) {
    println!("JS 传来消息:{}", invoke_message);
}

前端调用时,参数要放进一个对象里,且键名默认用驼峰(camelCase),对应 Rust 的蛇形(snake_case)会自动转换:

invoke('my_custom_command', { invokeMessage: '你好!' });

如果你更想在前端也用蛇形命名,可以在宏上声明 rename_all

#[tauri::command(rename_all = "snake_case")]
fn my_custom_command(invoke_message: String) {}
invoke('my_custom_command', { invoke_message: '你好!' });

参数类型只要实现了 serde::Deserialize(Rust 的序列化库),就能从 JSON 自动转成 Rust 值,常见的 Stringi32bool、结构体都支持。

返回值与异步命令

命令可以返回数据,只要类型实现了 serde::Serialize

#[tauri::command]
fn my_custom_command() -> String {
    "来自 Rust 的问候!".into()
}

前端拿到的是 Promise 解析出的值:

invoke('my_custom_command').then((message) => console.log(message));

如果命令要做耗时操作(读写文件、网络请求),应当声明成 async,否则会卡住界面。注意:异步命令里不能直接用借用的参数类型(如 &str),要把它换成 String,或者把返回类型包成 Result

// 用 String 代替 &str
#[tauri::command]
async fn my_custom_command(value: String) -> String {
    some_async_work().await;
    value
}
// 或包成 Result(适用于带 State 等借用类型)
#[tauri::command]
async fn my_custom_command(value: &str) -> Result<String, String> {
    some_async_work().await;
    Ok(value.to_string())
}

错误处理:返回 Result

真实命令可能失败。让函数返回 Result<T, E>,Tauri 会在出错时让前端的 Promise「拒绝(reject)」,成功时「兑现(resolve)」:

#[tauri::command]
fn login(user: String, password: String) -> Result<String, String> {
    if user == "tauri" && password == "tauri" {
        Ok("登录成功".to_string())
    } else {
        Err("凭据无效".to_string())
    }
}

前端用 try/catch.catch 接住错误:

try {
  const msg = await invoke('login', { user: 'tauri', password: 'tauri' });
  console.log(msg);
} catch (e) {
  console.error(e);
}
Tip

错误类型也必须能被序列化(实现 serde::Serialize)。新手图省事可以直接用 String 当错误类型;项目变大后,可借助 thiserror 这类 crate 定义自己的错误枚举,让错误信息更结构化。

把命令拆到独立模块

命令一多,lib.rs 会显得臃肿。官方推荐把命令放到单独的 commands.rs 文件。独立模块里的命令要标 pub

// src-tauri/src/commands.rs
#[tauri::command]
pub fn my_custom_command() {
    println!("我是从 JavaScript 被调用的!");
}

然后在 lib.rs 里声明模块,并带上 commands:: 前缀注册:

// src-tauri/src/lib.rs
mod commands;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![commands::my_custom_command])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

注意:命令名并不受模块作用域限制,仍要全局唯一。前端调用时还是写 invoke('my_custom_command'),那个 commands:: 前缀只是 Rust 侧的路径,前端看不到。

最小可运行示例(Rust + TS)

下面把前面要点拼成一个能直接跑的最小闭环。先写 Rust 端:

// src-tauri/src/lib.rs
#[tauri::command]
fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[tauri::command]
fn greet(name: String) -> String {
    format!("你好,{}!", name)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![add, greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

再写前端(以任意框架的入口文件为例,这里用纯 TS):

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

// 带参数、有返回值
const sum = await invoke('add', { a: 2, b: 3 });
console.log(sum); // 5

// 字符串参数、字符串返回
const reply = await invoke('greet', { name: '小明' });
console.log(reply); // 你好,小明!

别忘了 tauri.conf.jsonbuild 仍要对齐你的前端(devUrl 端口、frontendDist)。新增命令若涉及敏感能力,还要在 capabilities/ 里授权,否则前端调用会被安全模型拦下。

小结

Tauri 的命令机制三步走:用 #[tauri::command] 标记 Rust 函数;在 run() 里用 .invoke_handler(tauri::generate_handler![...]) 一次性注册所有命令(只能调一次);前端 import { invoke } from '@tauri-apps/api/core' 后用 invoke('命令名', { 参数 }) 调用。参数默认 camelCase,返回值和错误分别靠 serde::SerializeResult 表达;耗时操作用 async,命令多了就拆到独立模块。掌握这座桥,你就握住了 Tauri「用前端写界面、用 Rust 干重活」的核心。