從前端呼叫 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 回應),這可能會減慢您的應用程式速度。若要以最佳化方式傳回 array buffers,請使用 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 將會 rejected,否則,它會 resolves
invoke('login', { user: 'tauri', password: '0j4rijw8=' }) .then((message) => console.log(message)) .catch((error) => console.error(error));
如上所述,從命令傳回的所有內容都必須實作 serde::Serialize
,包括錯誤。如果您正在使用 Rust 標準函式庫或外部 crates 中的錯誤型別,這可能會造成問題,因為大多數錯誤型別都沒有實作它。在簡單的情況下,您可以使用 map_err
將這些錯誤轉換為 String
s
#[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, ()>
傳回字串,且沒有錯誤。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
物件,其中包含原始 body payload 和請求標頭。
#[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() 以傳送原始請求 body,並在第三個引數中包含請求標頭
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 payload。
全域事件
若要觸發全域事件,您可以使用 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 Contributors。CC-BY / MIT