从前端调用 Rust
本文档包含有关如何从应用程序前端与 Rust 代码通信的指南。 要了解如何从 Rust 代码与前端通信,请参阅 从 Rust 调用前端 。
Tauri 提供了一个 命令 原语,用于以类型安全的方式访问 Rust 函数,以及一个更加动态的 事件系统 。
Tauri 提供了一个简单但功能强大的 command 系统,用于从 Web 应用调用 Rust 函数。
命令可以接受参数并返回结果。它们还可以返回错误或是 async 执行。
您可以在 src-tauri/src/lib.rs 文件中定义命令。
要创建命令,只需添加一个函数并用 #[tauri::command] 注释它:
#[tauri::command]fn my_custom_command() {  println!("I was invoked from JavaScript!");}您必须向构建器函数提供命令列表,如下所示:
#[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");}现在,您可以从 JavaScript 代码调用该命令:
// 使用 Tauri API npm 包时:import { invoke } from '@tauri-apps/api/core';
// 使用 Tauri 全局脚本(不使用 npm 包时)// 确保在 `tauri.conf.json` 中设置 `app.withGlobalTauri` 为 trueconst invoke = window.__TAURI__.core.invoke;
// 调用命令invoke('my_custom_command');如果您的应用程序定义了很多组件或者对它们进行了分组,
那么您可以在单独的模块中定义命令,而不是使 lib.rs 文件变得臃肿。
作为示例,我们在 src-tauri/src/commands.rs 文件中定义一个命令:
#[tauri::command]pub fn my_custom_command() {  println!("I was invoked from JavaScript!");}在 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");}注意命令列表中的 commands:: 前缀,它表示命令函数的完整路径。
此示例中的命令名称是 my_custom_command ,因此您仍然可以通过执行
invoke("my_custom_command") 来调用它。在您的前端, commands:: 前缀将被忽略。
当前端使用不带参数的 invoke() 调用 Rust 时,您需要按如下方式调整前端代码。
原因是 Rust 不支持可选参数。
#[wasm_bindgen]extern "C" {    // 没有参数的 invoke    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)]    async fn invoke_without_args(cmd: &str) -> JsValue;
    // (默认)含参的 invoke    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])]    async fn invoke(cmd: &str, args: JsValue) -> JsValue;
    // 他们需要拥有不同的名字!}您的命令处理程序可以接受参数:
#[tauri::command]fn my_custom_command(invoke_message: String) {  println!("I was invoked from JavaScript, with this message: {}", invoke_message);}参数应以小驼峰命名,并作为 JSON 对象的键的传递:
invoke('my_custom_command', { invokeMessage: 'Hello!' });参数可以是任何类型,只要它们实现 serde::Deserialize 。
相应的 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 。
实现了 serde::Serialize 的返回值在将结果发送到前端时被序列化为 JSON。
如果您尝试返回大量数据(例如文件或下载 HTTP response ),这可能会减慢应用程序的速度。
要以优化的方式返回数组缓冲区,请使用 tauri::ipc::Response :
use tauri::ipc::Response;#[tauri::command]fn read_file() -> Response {  let data = std::fs::read("/path/to/file").unwrap();  tauri::ipc::Response::new(data)}如果您的处理程序可能失败并且需要能够返回错误,请让函数返回 Result :
#[tauri::command]fn login(user: String, password: String) -> Result<String, String> {  if user == "tauri" && password == "tauri" {    // resolve    Ok("logged_in".to_string())  } else {    // reject    Err("invalid credentials".to_string())  }}如果命令返回错误,则 promise 将抛出错误,否则,它将正常运行:
invoke('login', { user: 'tauri', password: '0j4rijw8=' })  .then((message) => console.log(message))  .catch((error) => console.error(error));如上所述,命令返回的所有内容都必须实现 serde::Serialize ,包括错误。
如果您使用 Rust 的 std 库或外部包中的错误类型,这可能会有问题,因为大多数错误类型都没有实现它。
在简单的场景中,您可以使用 map_err 将这些错误转换为 String :
#[tauri::command]fn my_custom_command() -> Result<(), String> {  std::fs::File::open("path/to/file").map_err(|err| err.to_string())?;  // 成功时返回 `null`  Ok(())}由于这不太符合惯用习惯,您可能需要创建自己的错误类型,该类型实现了 serde::Serialize 。
在下面的示例中,我们使用 thiserror crate 来帮助创建错误类型。
它允许您通过派生 thiserror::Error 特质将枚举转换为错误类型。
您可以查阅其文档以了解更多详细信息。
// 创建在我们程序中可能发生的所有错误#[derive(Debug, thiserror::Error)]enum Error {  #[error(transparent)]  Io(#[from] std::io::Error)}
// 我们需要手动实现 serde::Serializeimpl 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> {  // 这里将会返回一个错误  std::fs::File::open("path/that/does/not/exist")?;  // 成功时返回 `null`  Ok(())}自定义错误类型的优点是可以明确显示所有可能的错误,以便读者能够快速识别可能发生的错误。 这可以为其他人(以及您自己)在以后审查和重构代码时节省大量时间。
它还可以让你完全控制错误类型的序列化方式。在上面的例子中, 我们只是将错误消息作为字符串返回,但你可以为每个错误分配一个代码, 这样就可以更轻松地将其映射到类似的 TypeScript 错误枚举,例如:
#[derive(Debug, thiserror::Error)]enum Error {  #[error(transparent)]  Io(#[from] std::io::Error),  #[error("failed to parse as string: {0}")]  Utf8(#[from] std::str::Utf8Error),}
#[derive(serde::Serialize)]#[serde(tag = "kind", content = "message")]#[serde(rename_all = "camelCase")]enum ErrorKind {  Io(String),  Utf8(String),}
impl serde::Serialize for Error {  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>  where    S: serde::ser::Serializer,  {    let error_message = self.to_string();    let error_kind = match self {      Self::Io(_) => ErrorKind::Io(error_message),      Self::Utf8(_) => ErrorKind::Utf8(error_message),    };    error_kind.serialize(serializer)  }}
#[tauri::command]fn read() -> Result<Vec<u8>, Error> {  let data = std::fs::read("/path/to/file")?;  Ok(data)}在您的前端您现在会得到一个 { kind: 'io' | 'utf8', message: string } 错误对象:
type ErrorKind = {  kind: 'io' | 'utf8';  message: string;};
invoke('read').catch((e: ErrorKind) => {});Tauri 优先使用异步命令来执行繁重的工作,而不会导致 UI 冻结或减速。
如果您的命令需要异步运行,只需将其声明为 async 。
使用借用类型时,您必须进行额外的更改。以下是您的两个主要选项:
选项 1:将类型转换为非借用类型,例如 &str 转为 String 。这可能不适用于所有类型,例如 State<'_, Data> 。
例子:
// 在声明异步函数时使用 String 而不是 &str,因为 &str 是借用的,因此不支持#[tauri::command]async fn my_custom_command(value: String) -> String {  // 调用另一个异步函数并等待它完成  some_async_function().await;  value}选项 2:将返回类型包装在 Result 中。这个实现起来有点困难,但适用于所有类型。
使用返回类型 Result<a, b> ,将 a 替换为您希望返回的类型,或者 () 如果您希望返回 null ,
并将 b 替换为错误类型以在出现问题时返回,或者 () 如果您希望不返回可选错误。例如:
- Result<String, ()>返回一个 String ,且不会返回错误。
- Result<(), ()>返回- null。
- Result<bool, Error>返回一个 bool 值或一个错误,如上面的 错误处理 部分所示。
例子:
// 返回一个 Result<String, ()> 以绕过借用问题#[tauri::command]async fn my_custom_command(value: &str) -> Result<String, ()> {  // 调用另一个异步函数并等待它完成  some_async_function().await;  // 注意:返回值必须用 Ok() 包装。    Ok(format!(value))}由于从 JavaScript 调用命令已经返回一个 promise ,因此它的工作方式与任何其他命令一样:
invoke('my_custom_command', { value: 'Hello, Async!' }).then(() =>  console.log('Completed!'));Tauri 通道是推荐的流式数据传输机制,例如将 HTTP 响应流式传输到前端。 以下示例读取一个文件,并以 4096 字节为单位的块通知前端传输进度:
use tokio::io::AsyncReadExt;
#[tauri::command]async fn load_image(path: std::path::PathBuf, reader: tauri::ipc::Channel<&[u8]>) {  // 为了简单起见,本示例未包含错误处理  let mut file = tokio::fs::File::open(path).await.unwrap();
  let mut chunk = vec![0; 4096];
  loop {    let len = file.read(&mut chunk).await.unwrap();    if len == 0 {      // 读到文件末尾时结束循环      break;    }    reader.send(&chunk).unwrap();  }}请参阅 通道文档 以了解更多信息。
命令可以访问调用该消息的 WebviewWindow 实例:
#[tauri::command]async fn my_custom_command(webview_window: tauri::WebviewWindow) {  println!("WebviewWindow: {}", webview_window.label());}命令可以访问 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::Builder 上的 manage 函数来管理状态。
可以使用 tauri::State 在命令中访问状态:
struct MyState(String);
#[tauri::command]fn my_custom_command(state: tauri::State<MyState>) {  assert_eq!(state.0 == "some state value", true);}
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() {  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 命令还可以访问完整的 tauri::ipc::Request 对象,其中包括原始主体有效负载和请求标头。
#[derive(Debug, thiserror::Error)]enum Error {  #[error("unexpected request body")]  RequestBodyMustBeRaw,  #[error("missing `{0}` header")]  MissingHeader(&'static str),}
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 upload(request: tauri::ipc::Request) -> Result<(), Error> {  let tauri::ipc::InvokeBody::Raw(upload_data) = request.body() else {    return Err(Error::RequestBodyMustBeRaw);  };  let Some(authorization_header) = request.headers().get("Authorization") else {    return Err(Error::MissingHeader("Authorization"));  };
  // upload...
  Ok(())}在前端,您可以调用 invoke() 通过在 payload 参数上提供 ArrayBuffer
或 Uint8Array 来发送原始请求主体,并在第三个参数中包含请求标头:
const data = new Uint8Array([1, 2, 3]);await __TAURI__.core.invoke('upload', data, {  headers: {    Authorization: 'apikey',  },});tauri::generate_handler! 宏接受一个命令数组。要注册多个命令,你不能多次调用 invoke_handler。
只有最后一个将使用 call 。您必须将每个命令传递给 tauri::generate_handler! 。
#[tauri::command]fn cmd_a() -> String {  "Command a"}#[tauri::command]fn cmd_b() -> String {  "Command b"}
#[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("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())  }}
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() {  tauri::Builder::default()    .manage(Database {})    .invoke_handler(tauri::generate_handler![my_custom_command])    .run(tauri::generate_context!())    .expect("error while running tauri application");}import { invoke } from '@tauri-apps/api/core';
// 从 JavaScript 调用invoke('my_custom_command', {  number: 42,})  .then((res) =>    console.log(`Message: ${res.message}, Other Val: ${res.other_val}`)  )  .catch((e) => console.error(e));事件系统是前端和 Rust 之间更简单的通信机制。 与命令不同,事件不是类型安全的,始终是异步的,无法返回值,并且仅支持 JSON 格式的负载。
要触发全局事件,您可以使用 event.emit 或 WebviewWindow#emit 函数:
import { emit } from '@tauri-apps/api/event';import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
// emit(eventName, payload)emit('file-selected', '/path/to/file');
const appWebview = getCurrentWebviewWindow();appWebview.emit('route-changed', { url: window.location.href });要向特定 webview 注册的监听器触发事件,您可以使用 event.emitTo 或 WebviewWindow#emitTo 函数:
import { emitTo } from '@tauri-apps/api/event';import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
// emitTo(webviewLabel, eventName, payload)emitTo('settings', 'settings-update-requested', {  key: 'notification',  value: 'all',});
const appWebview = getCurrentWebviewWindow();appWebview.emitTo('editor', 'file-changed', {  path: '/path/to/file',  contents: 'file contents',});@tauri-apps/api NPM 包提供 API 来监听全局事件和特定于 webview 的事件。
- 
监听全局事件 import { listen } from '@tauri-apps/api/event';type DownloadStarted = {url: string;downloadId: number;contentLength: number;};listen<DownloadStarted>('download-started', (event) => {console.log(`downloading ${event.payload.contentLength} bytes from ${event.payload.url}`);});
- 
监听特定的 webview 事件 import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';const appWebview = getCurrentWebviewWindow();appWebview.listen<string>('logged-in', (event) => {localStorage.setItem('session-token', event.payload);});
listen 函数会在应用程序的整个生命周期内保持事件监听器的注册。
要停止监听某个事件,可以使用 listen 函数返回的 unlisten 函数:
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('download-started', (event) => {});unlisten();此外, Tauri 还提供了一个实用函数,用于精确监听一次事件:
import { once } from '@tauri-apps/api/event';import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
once('ready', (event) => {});
const appWebview = getCurrentWebviewWindow();appWebview.once('ready', () => {});全局事件和特定于 webview 的事件也会传递给在 Rust 中注册的监听器。
- 
监听全局事件 src-tauri/src/lib.rs use tauri::Listener;#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() {tauri::Builder::default().setup(|app| {app.listen("download-started", |event| {if let Ok(payload) = serde_json::from_str::<DownloadStarted>(&event.payload()) {println!("downloading {}", payload.url);}});Ok(())}).run(tauri::generate_context!()).expect("error while running tauri application");}
- 
监听特定的 webview 事件 src-tauri/src/lib.rs use tauri::{Listener, Manager};#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() {tauri::Builder::default().setup(|app| {let webview = app.get_webview_window("main").unwrap();webview.listen("logged-in", |event| {let session_token = event.data;// save token..});Ok(())}).run(tauri::generate_context!()).expect("error while running tauri application");}
listen 函数会在应用程序的整个生命周期内保持事件监听器的注册。
要停止监听某个事件,可以使用 unlisten 函数:
// 在事件处理程序作用域外取消监听:let event_id = app.listen("download-started", |event| {});app.unlisten(event_id);
// 当满足某些事件条件时取消监听:let handle = app.handle().clone();app.listen("status-changed", |event| {  if event.data == "ready" {    handle.unlisten(event.id);  }});此外, Tauri 还提供了一个实用函数,用于精确监听一次事件:
app.once("ready", |event| {  println!("app is ready");});在这种情况下,事件监听器在第一次触发后立即取消注册。
. 要了解如何从 Rust 代码中监听事件和发出事件,请参阅 Rust 事件系统文档 。© 2025 Tauri Contributors. CC-BY / MIT