跳到內容
Tauri

從前端呼叫 Rust

本文檔包含關於如何從您的應用程式前端與您的 Rust 程式碼進行通訊的指南。若要了解如何從您的 Rust 程式碼與您的前端進行通訊,請參閱從 Rust 呼叫前端

Tauri 提供了一個命令基本元件,用於以型別安全的方式存取 Rust 函數,以及一個更動態的事件系統

命令

Tauri 提供了一個簡單而強大的 command 系統,用於從您的 Web 應用程式呼叫 Rust 函數。命令可以接受引數和傳回值。它們也可以傳回錯誤並為 async

基本範例

命令可以在您的 src-tauri/src/lib.rs 檔案中定義。若要建立命令,只需新增一個函數並使用 #[tauri::command] 進行註解

src-tauri/src/lib.rs
#[tauri::command]
fn my_custom_command() {
println!("I was invoked from JavaScript!");
}

您必須像這樣向 builder 函數提供您的命令列表

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");
}

現在,您可以從您的 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 true
const invoke = window.__TAURI__.core.invoke;
// Invoke the command
invoke('my_custom_command');

在單獨的模組中定義命令

如果您的應用程式定義了許多組件,或者它們可以分組,您可以將命令定義在單獨的模組中,而不是使 lib.rs 檔案過於臃腫。

作為範例,讓我們在 src-tauri/src/commands.rs 檔案中定義一個命令

src-tauri/src/commands.rs
#[tauri::command]
pub fn my_custom_command() {
println!("I was invoked from JavaScript!");
}

lib.rs 檔案中,定義模組並相應地提供您的命令列表;

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");
}

請注意命令列表中的 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 將這些錯誤轉換為 Strings

#[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::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 `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 實例

src-tauri/src/lib.rs
#[tauri::command]
async fn my_custom_command(webview_window: tauri::WebviewWindow) {
println!("WebviewWindow: {}", webview_window.label());
}

在命令中存取 AppHandle

命令可以存取 AppHandle 實例

src-tauri/src/lib.rs
#[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 在命令上存取狀態

src-tauri/src/lib.rs
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! 的單次呼叫。

src-tauri/src/lib.rs
#[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");
}

完整範例

以上任何或所有功能都可以組合使用

src-tauri/src/lib.rs
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 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 payload。

全域事件

若要觸發全域事件,您可以使用 event.emitWebviewWindow#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.emitToWebviewWindow#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 matched
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