从前端调用 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!");}
你必须像这样向 builder 函数提供命令列表
#[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 代码中调用该命令
// When using the Tauri API npm package:import { invoke } from '@tauri-apps/api/core';
// When using the Tauri global script (if not using the npm package)// Be sure to set `app.withGlobalTauri` in `tauri.conf.json` to trueconst invoke = window.__TAURI__.core.invoke;
// Invoke the commandinvoke('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::
前缀将被忽略。
WASM
当使用 Rust 前端调用不带参数的 invoke()
时,你需要如下调整你的前端代码。原因是 Rust 不支持可选参数。
#[wasm_bindgen]extern "C" { // invoke without arguments #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)] async fn invoke_without_args(cmd: &str) -> JsValue;
// invoke with arguments (default) #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] async fn invoke(cmd: &str, args: JsValue) -> JsValue;
// They need to have different names!}
传递参数
你的命令处理程序可以接受参数
#[tauri::command]fn my_custom_command(invoke_message: String) { println!("I was invoked from JavaScript, with this message: {}", invoke_message);}
参数应作为带有 camelCase 键的 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,该 Promise 将使用返回值解析
invoke('my_custom_command').then((message) => console.log(message));
返回的数据可以是任何类型,只要它实现了 serde::Serialize
。
返回 Array Buffers
当响应发送到前端时,实现 serde::Serialize
的返回值会被序列化为 JSON。如果你尝试返回大型数据(例如文件或下载 HTTP 响应),这可能会减慢你的应用程序速度。要以优化的方式返回数组缓冲区,请使用 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 库或外部 crate 中的错误类型,这可能会有问题,因为大多数错误类型都没有实现它。在简单的情况下,你可以使用 map_err
将这些错误转换为 String
。
#[tauri::command]fn my_custom_command() -> Result<(), String> { std::fs::File::open("path/to/file").map_err(|err| err.to_string())?; // Return `null` on success Ok(())}
由于这不是很符合习惯,你可能需要创建自己的实现 serde::Serialize
的错误类型。在以下示例中,我们使用 thiserror
crate 来帮助创建错误类型。它允许你通过派生 thiserror::Error
trait 将枚举转换为错误类型。你可以查阅其文档以获取更多详细信息。
// 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::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> { // This will return an error std::fs::File::open("path/that/does/not/exist")?; // Return `null` on success 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>
。
示例
// Declare the async function using String instead of &str, as &str is borrowed and thus unsupported#[tauri::command]async fn my_custom_command(value: String) -> String { // Call another async function and wait for it to finish some_async_function().await; value}
选项 2:将返回类型包装在 Result
中。这种方法实现起来有点困难,但是适用于所有类型。
使用返回类型 Result<a, b>
,将 a
替换为你希望返回的类型,如果希望返回 null
,则替换为 ()
,并将 b
替换为在出现问题时要返回的错误类型,如果希望不返回可选错误,则替换为 ()
。例如
Result<String, ()>
返回 String,并且没有错误。Result<(), ()>
返回null
。Result<bool, Error>
返回布尔值或错误,如上面的 错误处理 部分所示。
示例
// Return a Result<String, ()> to bypass the borrowing issue#[tauri::command]async fn my_custom_command(value: &str) -> Result<String, ()> { // Call another async function and wait for it to finish some_async_function().await; // Note that the return value must be wrapped in `Ok()` now. Ok(format!(value))}
从 JavaScript 调用
由于从 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]>) { // for simplicity this example does not include error handling 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 { // Length of zero means end of file. break; } reader.send(&chunk).unwrap(); }}
有关更多信息,请参见 通道文档。
在命令中访问 WebviewWindow
命令可以访问调用消息的 WebviewWindow
实例
#[tauri::command]async fn my_custom_command(webview_window: tauri::WebviewWindow) { println!("WebviewWindow: {}", webview_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::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(())}
在前端,你可以通过在 payload 参数上提供 ArrayBuffer 或 Uint8Array 来调用 invoke() 发送原始请求正文,并在第三个参数中包含请求标头
const data = new Uint8Array([1, 2, 3]);await __TAURI__.core.invoke('upload', data, { headers: { Authorization: 'apikey', },});
创建多个命令
tauri::generate_handler!
宏接受命令数组。要注册多个命令,你不能多次调用 invoke_handler。只会使用最后一次调用。你必须将每个命令传递给 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';
// Invocation from JavaScriptinvoke('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 事件
要将事件触发到由特定 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
函数使事件监听器在应用程序的整个生命周期内保持注册状态。要停止监听事件,你可以使用 unlisten
函数,该函数由 listen
函数返回
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', () => {});
在 Rust 上监听事件
全局和 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
函数
// unlisten outside of the event handler scope:let event_id = app.listen("download-started", |event| {});app.unlisten(event_id);
// unlisten when some event criteria is matchedlet 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 贡献者。CC-BY / MIT