从前端调用 Rust

Tauri提供了一个简单而强大的 command 系统,用于从 web 应用程序调用 Rust 函数。 命令可以接受参数并返回值。 它们也可以返回错误并且是 async

基本示例

命令是在 src-tauri/src/main.rs 文件中定义的。 要创建一个命令,只需添加一个函数,并使用 #[tauri::command] 注释:


#[tauri::command]
fn my_custom_command() {
  println!("I was invoked from JS!");
}

必须向构建器函数提供一个命令列表,如下所示:


// 也是在main.rs
fn main() {
  tauri::Builder::default()
    // 在这里输入命令
    .invoke_handler(tauri::generate_handler![my_custom_command])
    .run(tauri::generate_context!())
    .expect("failed to run app");
}

现在,可以从 JS 代码中调用这个命令:


// 当使用 Tauri API npm 软件包时:
import { invoke } from '@tauri-apps/api/tauri'
// 使用Tauri全局脚本时(如果不使用 npm 软件包)
// 确保将 `tauri.conf.json` 中的 `build.withGlobalTauri` 设为 true
const invoke = window.__TAURI__.invoke

// 调用该命令
invoke('my_custom_command')

传递参数

命令处理程序可以接受参数:


#[tauri::command]
fn my_custom_command(invoke_message: String) {
  println!("I was invoked from JS, with this message: {}", invoke_message);
}

参数应该作为带有驼峰式键的 JSON 对象传递:


invoke('my_custom_command', { invokeMessage: 'Hello!' })

参数可以是任何类型,只要能实现 serde::Deserialize 即可。

请注意,在 Rust 中使用 snake_case 声明参数时,参数会转换为 JavaScript 的 camelCase(驼峰命名法)。

要在 JavaScript 中使用 snake_case,必须在 tauri::command 语句中声明:


#[tauri::command(rename_all = "snake_case")]
fn my_custom_command(invoke_message: String) {
  println!("I was invoked from JS, with this message: {}", invoke_message);
}

相应的 JavaScript:


invoke('my_custom_command', { invoke_message: 'Hello!' })

返回数据

命令处理程序也可以返回数据:


#[tauri::command]
fn my_custom_command() -> String {
  "Hello from Rust!".into()
}

invoke 函数返回一个用返回值解析的 promise:


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

返回的数据可以是任何类型,只要它实现了 serde::Serialize

错误处理

如果您的处理程序可能会失败,并且需要返回错误信息,那么该函数应返回一个Result



#[tauri::command]
fn my_custom_command() -> Result<String, String> {
  // If something fails
  Err("This failed!".into())
  // If it worked
  Ok("This worked!".into())
}


如果命令返回错误,promise 将拒绝,否则解析为:


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

如上所述,命令返回的所有内容都必须实现 serde::Serialize,包括错误。如果您使用的是 Rust 的 std 库或外部板块中的错误类型,这可能会造成问题,因为大多数错误类型都没有实现该功能。在简单的情况下,可以使用 map_err 将这些错误转换为字符串:



#[tauri::command]
fn my_custom_command() -> Result<(), String> {
  // This will return an error
  std::fs::File::open("path/that/does/not/exist").map_err(|err| err.to_string())?;
  // Return nothing on success
  Ok(())
}


由于这不是很习惯,你可能想创建自己的错误类型,实现 serde::Serialize。在下面的示例中,我们使用 thiserror crate 来帮助创建错误类型。它允许你通过派生 thiserror::Error 特质将枚举转化为错误类型。更多详情,请参阅其文档。



// create the error type that represents all errors possible in our program
#[derive(Debug, thiserror::Error)]
enum Error {
  #[error(transparent)]
  Io(#[from] std::io::Error)
}

// we must manually implement serde::Serialize
impl serde::Serialize for Error {
  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  where
    S: serde::ser::Serializer,
  {
    serializer.serialize_str(self.to_string().as_ref())
  }
}

#[tauri::command]
fn my_custom_command() -> Result<(), Error> {
  // This will return an error
  std::fs::File::open("path/that/does/not/exist")?;
  // Return nothing on success
  Ok(())
}


自定义错误类型的优点是可以明确列出所有可能的错误,这样读者就能快速识别可能发生的错误。这就为其他人(和你自己)在以后审查和重构代码时节省了大量时间。

它还能让你完全控制错误类型的序列化方式。在上面的示例中,我们只是以字符串形式返回错误信息,但您可以为每个错误分配一个类似于 C 语言的代码,这样您就可以更轻松地将其映射到类似于 TypeScript 错误枚举的外观。

异步命令

在 Tauri 中,异步函数有利于执行繁重的工作,而不会导致 UI 冻结或速度变慢。

备注

异步命令使用 async_runtime::spawn 在单线程上执行。 不带 async 关键字的命令将在主线程上执行,除非使用 #[tauri::command(async)] 定义。

如果命令需要异步运行,只需将其声明为 async 即可。

注意事项

使用 Tauri 创建异步函数时需要小心谨慎。目前,您不能简单地在异步函数的签名中包含借用参数。此类类型的一些常见例子是 &strState<'_, Data>。此处对这一限制进行了跟踪:https://github.com/tauri-apps/tauri/issues/2533 和变通方法如下所示。

在使用借用类型时,您必须进行额外的更改。以下是两个主要选项:

选项1: 将类型(如 &str)转换为未借用的类似类型(如 String)。这可能不适用于所有类型,例如 State<'_, Data>State<'_, Data>

示例:


// 声明异步函数时使用 String 而不是 &str,因为 &str 是借用的,因此不受支持
#[tauri::command]
async fn my_custom_command(value: String) -> String {
  // 调用另一个异步函数并等待其结束
  some_async_function().await;
  format!(value)
}

选项2: 将返回类型包裹在Result中。这个比较难实现,但应该适用于所有类型。

使用返回类型 Result<a, b>,将 a 替换为希望返回的类型,如果不希望返回任何类型,则替换为();将 b 替换为错误类型,如果出错,则返回错误类型,如果不希望返回可选错误,则替换为()。例如

  • Result<String, ()> 返回字符串,且无错误。
  • Result<(), ()> 不返回任何信息。
  • Result<bool, Error> 将返回布尔值或错误,如上文错误处理部分所示。

示例:



// 返回一个 Result<String, ()> 以绕过借用问题
#[tauri::command]
async fn my_custom_command(value: &str) -> Result<String, ()> {
  // 调用另一个异步函数并等待其结束
  some_async_function().await;
  // 请注意,现在必须用 `Ok()` 封装返回值。
  Ok(format!(value))
}


从 JS 调用

由于从 JavaScript 中调用该命令已经返回了一个promise,因此其工作方式与其他命令无异:


invoke('my_custom_command', { value: 'Hello, Async!' }).then(() =>
  console.log('Completed!')
)

在命令中访问窗口

命令可以访问调用消息的Window实例:


#[tauri::command]
async fn my_custom_command(window: tauri::Window) {
  println!("Window: {}", window.label());
}

在命令中访问 AppHandle

命令可以访问 AppHandle 实例:


#[tauri::command]
async fn my_custom_command(app_handle: tauri::AppHandle) {
  let app_dir = app_handle.path_resolver().app_dir();
  use tauri::GlobalShortcutManager;
  app_handle.global_shortcut_manager().register("CTRL + U", move || {});
}

访问管理状态

Tauri 可以使用 tauri::Buildermanage 函数管理状态。使用 tauri::State 命令可以访问状态:



struct MyState(String);

#[tauri::command]
fn my_custom_command(state: tauri::State<MyState>) {
  assert_eq!(state.0 == "some state value", true);
}

fn main() {
  tauri::Builder::default()
    .manage(MyState("some state value".into()))
    .invoke_handler(tauri::generate_handler![my_custom_command])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}


创建多个命令

tauri::generate_handler! 宏接收一个命令数组。要注册多个命令,不能多次调用 invoke_handler。只有最后一次调用才会被使用。必须将每条命令传递给 tauri::generate_handler!


#[tauri::command]
fn cmd_a() -> String {
    "Command a"
}
#[tauri::command]
fn cmd_b() -> String {
    "Command b"
}

fn main() {
  tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![cmd_a, cmd_b])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

完整示例

上述任何或所有功能均可组合使用:




struct Database;

#[derive(serde::Serialize)]
struct CustomResponse {
  message: String,
  other_val: usize,
}

async fn some_other_function() -> Option<String> {
  Some("response".into())
}

#[tauri::command]
async fn my_custom_command(
  window: tauri::Window,
  number: usize,
  database: tauri::State<'_, Database>,
) -> Result<CustomResponse, String> {
  println!("Called from {}", window.label());
  let result: Option<String> = some_other_function().await;
  if let Some(message) = result {
    Ok(CustomResponse {
      message,
      other_val: 42 + number,
    })
  } else {
    Err("No result".into())
  }
}

fn main() {
  tauri::Builder::default()
    .manage(Database {})
    .invoke_handler(tauri::generate_handler![my_custom_command])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}