跳至主要內容

從前端呼叫 Rust

Tauri 提供一個簡單但強大的 command 系統,用於從你的網路應用程式呼叫 Rust 函式。指令可以接受參數並傳回值。它們也可以傳回錯誤並為 async

基本範例

指令定義在你的 src-tauri/src/main.rs 檔案中。若要建立指令,只需新增一個函式並使用 #[tauri::command] 加以註解

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

你必須像這樣提供指令清單給建構函式

// Also in main.rs
fn main() {
tauri::Builder::default()
// This is where you pass in your commands
.invoke_handler(tauri::generate_handler![my_custom_command])
.run(tauri::generate_context!())
.expect("failed to run app");
}

現在,你可以從你的 JS 程式碼呼叫指令

// When using the Tauri API npm package:
import { invoke } from '@tauri-apps/api/tauri'
// When using the Tauri global script (if not using the npm package)
// Be sure to set `build.withGlobalTauri` in `tauri.conf.json` to true
const invoke = window.__TAURI__.invoke

// Invoke the command
invoke('my_custom_command')

傳遞參數

你的指令處理常式可以接受參數

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

參數應傳遞為具有 camelCase 鍵的 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 函式傳回一個承諾,並使用傳回值來解析

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())
}

如果指令傳回錯誤,承諾將會拒絕,否則,它會解析

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

如上所述,命令返回的所有內容都必須實作 serde::Serialize,包括錯誤。如果您使用 Rust 標準函式庫或外部 crate 的錯誤類型,這可能會造成問題,因為大多數錯誤類型都沒有實作它。在簡單的場景中,您可以使用 map_err 將這些錯誤轉換為 String

#[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>

範例

// 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;
format!(value)
}

選項 2:將回傳類型包裝在 Result 中。這個方法的實作難度稍高,但應該適用於所有類型。

使用回傳類型 Result<a, b>,將 a 替換為您想要回傳的類型,或 () 如果您想要回傳空值,並將 b 替換為如果發生錯誤時要回傳的錯誤類型,或 () 如果您想要沒有選用錯誤回傳。例如

  • Result<String, ()> 回傳字串,沒有錯誤。
  • Result<(), ()> 回傳空值。
  • 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))
}

從 JS 呼叫

由於從 JavaScript 呼叫指令已經會回傳承諾,因此其運作方式就像任何其他指令一樣

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

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");
}
// Invocation from JS

invoke('my_custom_command', {
number: 42,
})
.then((res) =>
console.log(`Message: ${res.message}, Other Val: ${res.other_val}`)
)
.catch((e) => console.error(e))