iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
Rust

用 Tauri 打造你的應用程式系列 第 9

[Day 09] 網頁前端與 Rust 後端之間的溝通 (三):Event

  • 分享至 

  • xImage
  •  

我們已經學會了如何使用 Command 從前端主動呼叫後端,但這只是故事的一半。在許多場景下,我們需要後端能夠「主動出擊」,在特定事件發生時通知前端,而不是總是等待前端的請求。這時候就需要使用 Event。

Event 的核心概念

Tauri 的 Event 系統提供了一種非同步的訊息傳遞機制。與 Command 最大的不同在於,Event 是 「即發即棄」(fire-and-forget) 的。

當後端發出一個事件後,它不會等待前端的回應,也不關心前端是否收到了這個事件,或者收到後做了什麼處理。它就像一個廣播員,向所有正在收聽特定頻道的聽眾(前端)發送訊息,然後就繼續做自己的事了。

這種模式非常適合以下場景:

  • 後端任務進度更新
  • 即時通知(例如:收到新訊息、系統狀態改變)
  • 從一個視窗通知另一個視窗發生了某件事

基礎實作:從後端發送事件

1. 後端 (Rust) 發送事件

在 Rust 後端,我們可以透過 AppHandleWindow 物件來發送事件。事件可以攜帶一個可序列化的資料負載(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;
    }
}

重點說明:

  1. 我們定義了一個 ProgressPayload 結構體,它必須 #[derive(Clone, serde::Serialize)],這樣 Tauri 才能將它序列化並傳遞給前端。
  2. start_long_task 這個 Command 中,我們執行了一個從 1 到 100 的迴圈,模擬一個長時間執行的任務,每次迭代都會發送一個進度更新事件。
  3. 使用 app_handle.emit() 來發送事件。第一個參數是事件的名稱("task-progress"),第二個參數是可選的 payload。

2. 前端 (JavaScript/TypeScript) 監聽事件

在 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 也可以雙向溝通

雖然 Event 主要用於後端到前端的溝通,但它其實是雙向的。前端同樣可以發送事件,讓後端或其他視窗來監聽。讓我們來實作一個情境:前端點擊一個按鈕,發送一個事件給後端。後端收到後,等待三秒,然後在自己的終端機印出一條訊息。

1. 前端 (JavaScript/TypeScript) 發送事件

我們使用 @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。

2. 後端 (Rust) 監聽事件

後端監聽事件的最佳位置是在應用程式啟動時的 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。
  • 接著,我們啟動一個新的執行緒,在裡面等待 3 秒後再印出最終訊息。這點非常重要,因為如果不這麼做,等待的 3 秒鐘將會阻塞 Tauri 的核心事件迴圈,導致你的應用程式卡死。

現在,當你在前端點擊按鈕,後端的終端機就會立刻印出收到的 payload,然後在 3 秒後印出確認訊息。

該用 Command 還是 Event?

既然 Command 和 Event 都可以讓前端與後端溝通,那實際開發時,應該怎麼選擇?

這取決於這個功能需要的是「請求-回應」模型,還是「發布-訂閱」模型。簡單的判斷方法是:「呼叫後端之後,需要用 await 來等待它的直接結果嗎?

  • 如果答案是「」,表示需要一個回應,請使用 Command。
  • 如果答案是「」,表示只想觸發一個流程或廣播一個狀態,請使用 Event。

小結

透過 Events 事件系統,我們補全了 Tauri 應用的通訊拼圖。結合 Command 的「請求-回應」模式和 Event 的「發布-訂閱」模式,我們擁有了一套完整的雙向溝通框架,基本上就能應對從簡單工具到複雜桌面應用的各種需求了。


上一篇
[Day 07] 網頁前端與 Rust 後端之間的溝通 (二):Command
下一篇
[Day 10] 狀態管理:在 Rust 後端共享資料 (State)
系列文
用 Tauri 打造你的應用程式10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言