Tauriの基礎 - イベントの送受信

提供: MochiuWiki : SUSE, EC, PCB

概要

Tauriのイベントシステムは、RustバックエンドとWebフロントエンド間の双方向通信を実現する軽量なメカニズムである。
Commandsとは異なり、イベントは戻り値を必要としない通知型の通信に適しており、バックエンドからフロントエンドへのプッシュ通知や、コンポーネント間の状態共有に活用される。

下表に、イベントシステムの主な特徴を示す。

Tauriイベントシステムの特徴
特徴 説明
非同期通信 送信側は受信側の応答を待たずに処理を継続できる。
一方向の通知として動作し、fire-and-forgetパターンに適している。
軽量 JSONベースのシリアライゼーションにより、オーバーヘッドが小さい。
高頻度なイベント送信にも対応できる。
柔軟なターゲティング 全リスナーへのブロードキャスト、特定WebViewへのユニキャスト、フィルタリング送信が可能である。
アプリケーションの要件に応じた送信先制御ができる。
型安全性 Rustの Serialize トレイトとTypeScriptの型定義により、型安全な通信が実現できる。
コンパイル時の型チェックでランタイムエラーを防止できる。


イベントはフロントエンド (JavaScript / TypeScript) からRustバックエンドへの送信、およびRustバックエンドからフロントエンドへの送信の両方向で利用できる。


イベントシステムの基本

Commandsとの違い

Tauriには、フロントエンドとバックエンド間の通信手段として、CommandsEvents の2つが存在する。

下表に、それぞれの特徴を示す。

CommandsとEventsの比較
特徴 Commands Events
通信パターン リクエスト・レスポンス型 通知型 (fire-and-forget)
戻り値 あり (Promiseで受信) なし
主な用途 データの取得、処理の実行 状態通知、プログレス更新、イベント通知
送信方向 フロントエンド → バックエンド 双方向
待機 呼び出し側は完了を待機 送信側は応答を待たない


使用シーンの例

イベントシステムが適しているシーンを以下に示す。

  • ダウンロード進捗の通知
    バックエンドでダウンロードが進行するたびに、進捗率をフロントエンドに通知する。
  • ファイル変更の監視
    ファイルシステムの変更を検知し、フロントエンドに変更内容を通知する。
  • ログイン状態の同期
    認証完了時にセッショントークンを全WebViewに通知する。
  • バックグラウンド処理の完了通知
    長時間の処理が完了したタイミングでユーザーに通知する。



emit : イベントの送信

Rustからの送信

Rustバックエンドからイベントを送信するには、AppHandleemit メソッドを使用する。

emit は全てのリスナーにイベントをブロードキャストする。

基本的な使用例を以下に示す。

 use tauri::{AppHandle, Emitter};
 
 #[tauri::command]
 fn start_download(app: AppHandle, url: String) {
    // イベント名とペイロードを指定して送信
    // emit()は全リスナーにブロードキャストする
    app.emit("download-started", &url).unwrap();
 
    // 進捗状況の通知
    for progress in [25, 50, 75, 100] {
       app.emit("download-progress", progress).unwrap();
    }
 
    // 完了通知
    app.emit("download-finished", &url).unwrap();
 }


フロントエンドからの送信

フロントエンド (TypeScript / JavaScript) からイベントを送信するには、@tauri-apps/api/eventemit 関数を使用する。

 import { emit } from '@tauri-apps/api/event';
 
 // グローバルイベントの送信
 // 第1引数: イベント名、第2引数: ペイロード
 emit('file-selected', '/path/to/file');
 
 // オブジェクトをペイロードとして送信
 emit('user-action', {
   action: 'click',
   target: 'submit-button',
   timestamp: Date.now()
 });


WebView固有のイベント送信

現在のWebViewからイベントを送信するには、getCurrentWebviewWindow を使用する。

 import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
 
 const appWebview = getCurrentWebviewWindow();
 
 // 現在のWebViewにイベントを送信
 appWebview.emit('route-changed', { url: window.location.href });



listen : イベントの受信

フロントエンドでの受信

フロントエンドでイベントを受信するには、listen 関数を使用する。

listen はグローバルイベントをリッスンし、イベントが発生するたびにコールバック関数が実行される。

基本的な使用例を以下に示す。

 import { listen } from '@tauri-apps/api/event';
 
 // イベントリスナーの登録
 // 戻り値は unlisten 関数 (後でリスナーを解除する時に使用)
 const unlisten = await listen<string>('download-progress', (event) => {
   console.log(`ダウンロード進捗: ${event.payload}%`);
 });
 
 // 使用後にリスナーを解除する
 // unlisten();


型付きペイロードの受信

listen 関数はジェネリクスをサポートしており、ペイロードの型を指定できる。

 import { listen } from '@tauri-apps/api/event';
 
 // ペイロードの型を定義
 interface DownloadStarted {
   url: string;
   downloadId: number;
   contentLength: number;
 }
 
 // 型を指定してリッスン
 const unlisten = await listen<DownloadStarted>('download-started', (event) => {
   console.log(`ダウンロード開始: ${event.payload.contentLength} バイト`);
   console.log(`URL: ${event.payload.url}`);
   console.log(`ID: ${event.payload.downloadId}`);
 });


WebView固有イベントの受信

特定のWebView宛てのイベントを受信するには、getCurrentWebviewWindowlisten メソッドを使用する。

 import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
 
 const appWebview = getCurrentWebviewWindow();
 
 // WebView固有のイベントをリッスン
 appWebview.listen<string>('logged-in', (event) => {
   // ログイン時にセッショントークンを受け取る
   localStorage.setItem('session-token', event.payload);
 });


Rustでの受信

Rustバックエンドでもイベントをリッスンできる。

 use tauri::Manager;
 
 fn setup_listeners(app: &tauri::AppHandle) {
    // イベントリスナーの登録
    app.listen("frontend-event", |event| {
       println!("イベントを受信: {:?}", event.payload());
    });
 }



unlisten : リスナーの解除

解除の重要性

イベントリスナーは、不要になった時点で必ず解除する必要がある。

特にSPA (Single Page Application) では、ページ遷移してもリスナーが自動的に解除されないため、メモリリークの原因となる。

フロントエンドでの解除

listen 関数は unlisten 関数を返す。
この関数を呼び出すことでリスナーを解除できる。

 import { listen } from '@tauri-apps/api/event';
 
 // リスナーの登録
 const unlisten = await listen('my-event', (event) => {
   console.log('イベントを受信:', event.payload);
 });
 
 // リスナーの解除
 // これ以降、イベントを受信しなくなる
 unlisten();


Rustでの解除

Rustでは、listen が返す EventHandlerunlisten に渡して解除する。

 use tauri::Manager;
 
 fn manage_listener(app: &tauri::AppHandle) {
    // リスナーの登録 (EventHandler を取得)
    let handler = app.listen("status-changed", |event| {
       println!("ステータス変更: {:?}", event.payload());
    });
 
    // リスナーの解除
    app.unlisten(handler);
 }


条件付きでの解除

イベントハンドラ内で条件に基づいてリスナーを解除することもできる。

 use tauri::Manager;
 
 fn conditional_unlisten(app: &tauri::AppHandle) {
    let handle = app.handle().clone();
 
    app.listen("status-changed", move |event| {
       // 条件に一致したらリスナーを解除
       if event.payload() == "\"ready\"" {
          println!("準備完了 - リスナーを解除");
          handle.unlisten(event.id);
       }
    });
 }



emit_to : 特定WebViewへの送信

emit_to を使用すると、特定のWebViewにのみイベントを送信できる。

複数のウインドウやWebViewが存在するアプリケーションで、特定のウインドウにのみ通知を送りたい場合に便利である。

Rustからの送信

 use tauri::{AppHandle, Emitter};
 
 #[tauri::command]
 fn login(app: AppHandle, user: String, password: String) {
    // 認証処理
    let authenticated = user == "admin" && password == "password";
    let result = if authenticated { "loggedIn" } else { "invalidCredentials" };
 
    // "login" WebViewにのみ結果を送信
    // 第1引数: WebViewのラベル、第2引数: イベント名、第3引数: ペイロード
    app.emit_to("login", "login-result", result).unwrap();
 }


フロントエンドからの送信

 import { emitTo } from '@tauri-apps/api/event';
 import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
 
 // 方法1 : emitTo 関数を使用
 // 第1引数: WebViewラベル、第2引数: イベント名、第3引数: ペイロード
 emitTo('settings', 'settings-update-requested', {
   key: 'notification',
   value: 'all'
 });
 
 // 方法2 : WebviewWindow インスタンスから送信
 const appWebview = getCurrentWebviewWindow();
 appWebview.emitTo('editor', 'file-changed', {
   path: '/path/to/file',
   contents: 'file contents'
 });



emit_filter : フィルタリング送信

概要

emit_filter を使用すると、条件に基づいて特定のWebViewにのみイベントを送信できる。

複雑な条件で送信先を制御したい場合に使用する。

使用例

 use tauri::{AppHandle, Emitter, EventTarget};
 use std::path::PathBuf;
 
 #[tauri::command]
 fn open_file(app: AppHandle, path: PathBuf) {
    // フィルタリングして特定のWebViewにのみ送信
    // "main" または "file-viewer" WebViewにのみ送信
    app.emit_filter("open-file", &path, |target| {
       match target {
          EventTarget::WebviewWindow { label } => {
             label == "main" || label == "file-viewer"
          },
          _ => false  // 他のターゲットには送信しない
       }
    }).unwrap();
 }


EventTarget の種類

下表に、emit_filter のフィルタ関数で使用できる EventTarget の主な種類を示す。

EventTarget の種類
ターゲット 説明
EventTarget::App アプリケーション自体
EventTarget::WebviewWindow { label } 特定のWebViewウインドウ
EventTarget::Webview { label } 特定のWebView
EventTarget::Window { label } 特定のウインドウ



イベント名の命名規則

使用可能な文字

イベント名には、以下に示す文字を使用できる。

  • 英数字 (a-z, A-Z, 0-9)
    大文字と小文字は区別される。
  • ハイフン (-)
    単語の区切りに使用する。
  • アンダースコア (_)
    スネークケース記法に使用する。


推奨される命名規則

イベント名は、イベントの内容と送信元がわかるように命名することを推奨する。

推奨されるパターンを以下に示す。

  • ケバブケース (kebab-case) (推奨)
    download-started、file-changed、user-logged-in
  • スネークケース (snake_case)
    download_started、file_changed
  • ドメインプレフィックス付き
    auth:login-success、file:change-detected


避けるべき命名

  • 空白を含む名前
    download started (不正)
  • 特殊文字を含む名前
    download@started、download#progress (不正)
  • あいまいな名前
    event1、data、update (非推奨)



グローバルイベント

グローバルイベントは、アプリケーション全体で共有されるイベントである。
emit で送信されたイベントは、全てのグローバルリスナーに配信される。

使用シーン

  • アプリケーション全体の状態通知
    テーマ変更、言語設定の変更等
  • 複数ウインドウ間の通信
    あるウインドウでの操作を他のウインドウに通知する。
  • バックエンドからのブロードキャスト
    全フロントエンドに一斉通知を行う。


listen_any : 全イベントの受信

全てのイベント (フィルタやターゲットに関係なく) を受信するには、listen_any を使用する。

 import { listenAny } from '@tauri-apps/api/event';
 
 // 全てのイベントをリッスン
 const unlisten = await listenAny((event) => {
   console.log(`イベント受信: ${event.event}`);
   console.log(`ペイロード:`, event.payload);
 });



サンプルコード

ダウンロードマネージャーの例

Rustバックエンドでダウンロードを管理して、フロントエンドに進捗を通知する例を以下に示す。

Rust (バックエンド)
 use tauri::{AppHandle, Emitter};
 use serde::Serialize;
 
 // イベントペイロードの型定義
 #[derive(Clone, Serialize)]
 #[serde(rename_all = "camelCase")]
 struct DownloadStarted {
    url: String,
    download_id: usize,
    content_length: usize,
 }
 
 #[derive(Clone, Serialize)]
 #[serde(rename_all = "camelCase")]
 struct DownloadProgress {
    download_id: usize,
    chunk_length: usize,
 }
 
 #[derive(Clone, Serialize)]
 #[serde(rename_all = "camelCase")]
 struct DownloadFinished {
    download_id: usize,
 }
 
 #[tauri::command]
 fn download_file(app: AppHandle, url: String) {
    let download_id = 1;
    let content_length = 10000;  // 実際はHTTPヘッダーから取得
 
    // ダウンロード開始通知
    app.emit("download-started", DownloadStarted {
       url: url.clone(),
       download_id,
       content_length,
    }).unwrap();
 
    // ダウンロード進捗のシミュレーション
    for chunk_length in [1500, 2500, 3000, 2000, 1000] {
       app.emit("download-progress", DownloadProgress {
          download_id,
          chunk_length,
       }).unwrap();
    }
 
    // ダウンロード完了通知
    app.emit("download-finished", DownloadFinished {
       download_id,
    }).unwrap();
 }


TypeScript (フロントエンド)
 import { listen } from '@tauri-apps/api/event';
 
 // イベントペイロードの型定義
 interface DownloadStarted {
   url: string;
   downloadId: number;
   contentLength: number;
 }
 
 interface DownloadProgress {
   downloadId: number;
   chunkLength: number;
 }
 
 interface DownloadFinished {
   downloadId: number;
 }
 
 // ダウンロードイベントのリスナーを設定
 async function setupDownloadListeners() {
   let totalDownloaded = 0;
   let expectedLength = 0;
 
   // ダウンロード開始イベント
   const unlistenStart = await listen<DownloadStarted>('download-started', (event) => {
     console.log(`ダウンロード開始: ${event.payload.url}`);
     expectedLength = event.payload.contentLength;
     totalDownloaded = 0;
   });
 
   // ダウンロード進捗イベント
   const unlistenProgress = await listen<DownloadProgress>('download-progress', (event) => {
     totalDownloaded += event.payload.chunkLength;
     const percentage = (totalDownloaded / expectedLength) * 100;
     console.log(`進捗: ${percentage.toFixed(1)}%`);
   });
  
   // ダウンロード完了イベント
   const unlistenFinished = await listen<DownloadFinished>('download-finished', (event) => {
     console.log(`ダウンロード完了: ID ${event.payload.downloadId}`);
 
     // 不要になったリスナーを解除
     unlistenStart();
     unlistenProgress();
     unlistenFinished();
   });
 }
 
 // 初期化時に呼び出し
 setupDownloadListeners();



注意事項

パフォーマンスへの考慮

  • 高頻度のイベント送信は避ける。
    短時間に大量のイベントを送信すると、パフォーマンスに影響する。
    必要に応じてスロットリングやデバウンスを検討する。
  • ペイロードのサイズを最小限にする。
    大きなオブジェクトを送信するとシリアライゼーションに時間が掛かる。
    必要なデータのみを含める。


エラーハンドリング

  • emit のエラーを適切に処理する。
    emitResult を返すため、エラーハンドリングを行う。


 // 良い例 : エラーハンドリング
 if let Err(e) = app.emit("my-event", &payload) {
    eprintln!("イベント送信エラー: {}", e);
 }
 
 // 悪い例 : エラーを無視
 app.emit("my-event", &payload).unwrap();  // パニックの可能性


SPAでの注意点

SPA (Single Page Application) では、ページ遷移してもリスナーが自動的に解除されない。

コンポーネントのアンマウント時に必ず unlisten を呼び出すこと。

詳細を知りたい場合は、Tauriの基礎 - 型付きイベントとパターンのページを参照すること。

once : 1度だけリッスン

イベントを1度だけ受信したい場合は、once 関数を使用する。

 import { once } from '@tauri-apps/api/event';
 
 // イベントを一度だけ受信
 await once('app-ready', (event) => {
   console.log('アプリケーションの準備が完了しました');
 });


 use tauri::Manager;
 
 fn setup(app: &tauri::AppHandle) {
    // Rust での once
    app.once("ready", |event| {
       println!("アプリケーション準備完了");
    });
 }



参考リンク