我們已經學會了如何使用 Command 從前端主動呼叫後端,但這只是故事的一半。在許多場景下,我們需要後端能夠「主動出擊」,在特定事件發生時通知前端,而不是總是等待前端的請求。這時候就需要使用 Event。
Tauri 的 Event 系統提供了一種非同步的訊息傳遞機制。與 Command
最大的不同在於,Event 是 「即發即棄」(fire-and-forget) 的。
當後端發出一個事件後,它不會等待前端的回應,也不關心前端是否收到了這個事件,或者收到後做了什麼處理。它就像一個廣播員,向所有正在收聽特定頻道的聽眾(前端)發送訊息,然後就繼續做自己的事了。
這種模式非常適合以下場景:
在 Rust 後端,我們可以透過 AppHandle
或 Window
物件來發送事件。事件可以攜帶一個可序列化的資料負載(payload)。
use serde::Serialize;
use tokio::time::{sleep, Duration};
// 定義一個可序列化的結構體作為事件的資料負載
#[derive(Clone, Serialize)]
struct ProgressPayload {
percentage: u8,
message: String,
}
#[tauri::command]
async fn start_long_task(app_handle: tauri::AppHandle) {
for i in 1..=100 {
app_handle.emit("task-progress", ProgressPayload {
percentage: i,
message: format!("進度: {}%", i),
}).unwrap();
sleep(Duration::from_millis(50)).await;
}
}
重點說明:
ProgressPayload
結構體,它必須 #[derive(Clone, serde::Serialize)]
,這樣 Tauri 才能將它序列化並傳遞給前端。start_long_task
這個 Command 中,我們執行了一個從 1 到 100 的迴圈,模擬一個長時間執行的任務,每次迭代都會發送一個進度更新事件。app_handle.emit()
來發送事件。第一個參數是事件的名稱("task-progress"
),第二個參數是可選的 payload。在 Vue 前端,我們使用 @tauri-apps/api/event
中的 listen
函式來註冊事件監聽器。
import { ref, onMounted, onUnmounted } from 'vue'
import { listen, UnlistenFn } from '@tauri-apps/api/event'
// 定義後端傳來的 payload 型別
interface ProgressPayload {
percentage: number;
message: string;
}
const progressMessage = ref<string>('尚未開始')
let unlistenProgress: UnlistenFn | null = null;
onMounted(async () => {
// 監聽名為 'task-progress' 的事件
unlistenProgress = await listen<ProgressPayload>('task-progress', (event) => {
// event.payload 就是後端傳來的 ProgressPayload 物件
progressMessage.value = event.payload.message;
});
})
// 非常重要:在元件銷毀時,必須取消監聽以避免記憶體洩漏
onUnmounted(() => {
if (unlistenProgress) {
unlistenProgress();
}
})
前端的 listen
函式會回傳一個 unlisten
函式。當你的元件(例如一個 Vue 頁面)被銷毀時,你必須呼叫這個 unlisten
函式來移除監聽器。如果忘記這一步,監聽器會殘留在記憶體中,即使元件已經消失,每次後端發送事件時它仍然會被觸發,從而導致嚴重的記憶體洩漏問題。
在 Vue 中,最好的作法是將清理邏輯放在 onUnmounted
Lifecycle Hooks 中,確保萬無一失。
雖然 Event 主要用於後端到前端的溝通,但它其實是雙向的。前端同樣可以發送事件,讓後端或其他視窗來監聽。讓我們來實作一個情境:前端點擊一個按鈕,發送一個事件給後端。後端收到後,等待三秒,然後在自己的終端機印出一條訊息。
我們使用 @tauri-apps/api/event
中的 emit
函式。
// src/main.ts 或你的前端組件中
import { emit } from '@tauri-apps/api/event';
// 定義發送給後端的 payload 型別
interface FrontendEventPayload {
message: string;
}
function notifyBackend(): void {
console.log('Notifying backend...');
emit('frontend-event', {
message: '嗨!後端,這是一則來自前端的訊息!'
} as FrontendEventPayload);
}
並在 <template>
中加入一個按鈕綁定 click 事件來處發 notifyBackend
:
<button @click="notifyBackend">Notify Backend</button>
這裡我們發送了一個名為 frontend-event
的事件,並附帶了一個簡單的 payload。
後端監聽事件的最佳位置是在應用程式啟動時的 setup
hook 中,這樣可以確保監聽器在整個應用程式生命週期內都處於活動狀態。
// src-tauri/src/main.rs
fn main() {
tauri::Builder::default()
.setup(|app| {
// 取得 AppHandle,用於監聽事件
let app_handle = app.handle().clone();
// 監聽來自前端的 'frontend-event'
app_handle.listen("frontend-event", |event| {
println!("Got 'frontend-event' with payload: {:?}", event.payload());
// 啟動一個新執行緒來處理延遲任務,避免阻塞主執行緒
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(3));
println!("延遲 3 秒後... 後端確認收到了來自前端的訊息!");
});
});
Ok(())
})
.invoke_handler(tauri::generate_handler![start_background_task])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
重點說明:
tauri::Builder
後面串接了 .setup()
方法。這個閉包會在應用程式初始化時執行。setup
裡,我們使用 app.handle().listen()
來註冊一個全域的事件監聽器。frontend-event
的事件時,會先印出收到的 payload。現在,當你在前端點擊按鈕,後端的終端機就會立刻印出收到的 payload,然後在 3 秒後印出確認訊息。
既然 Command 和 Event 都可以讓前端與後端溝通,那實際開發時,應該怎麼選擇?
這取決於這個功能需要的是「請求-回應」模型,還是「發布-訂閱」模型。簡單的判斷方法是:「呼叫後端之後,需要用 await
來等待它的直接結果嗎?」
透過 Events 事件系統,我們補全了 Tauri 應用的通訊拼圖。結合 Command 的「請求-回應」模式和 Event 的「發布-訂閱」模式,我們擁有了一套完整的雙向溝通框架,基本上就能應對從簡單工具到複雜桌面應用的各種需求了。