mcpField
L
listening-to-tauri-events

listening-to-tauri-events

github.com/dchuk開発36171 回インストール40 週間前に更新
リポジトリ

listening-to-tauri-events について

Tauri v2 のフロントエンド開発者向けに、Rust が発行するイベントの購読方法を解説します。グローバル・Webview 向けのリスナー、型付きペイロード、一度限りの購読、後処理を扱います。

主な機能

  1. 01グローバルイベントの購読
  2. 02Webview 専用リスナー
  3. 03型付きイベントペイロード
  4. 04一度限りのリスナー
  5. 05リスナーの手動解除
  6. 06Rust からのイベント発行例

活用シーン

  1. 01Rust バックエンドの更新受信
  2. 02特定ウィンドウ向けイベントの処理
  3. 03SPA コンポーネントでのリスナー解除
  4. 04TypeScript のイベントデータ型の指定

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dchuk/claude-code-tauri-skills --skill listening-to-tauri-events

インストール後、Claude Code で/listening-to-tauri-eventsを入力して読み込まれたか確認します。

詳細

Tauri のフロントエンドでイベントをリッスンする

このスキルでは、@tauri-apps/api/event モジュールを使って、Tauri v2 アプリケーションで Rust から発行されたイベントをサブスクライブする方法を説明します。

基本概念

Tauri には 2 種類のイベントスコープがあります。

  1. グローバルイベント - すべての WebView のリスナーにブロードキャストされます
  2. WebView 固有のイベント - 特定の WebView ウィンドウを対象とします

重要: WebView 固有のイベントは、グローバルリスナーでは受信されません。Rust からのイベントの発行方法に応じて、適切なリスナータイプを使用してください。

インストール

イベント API は Tauri のコア API パッケージに含まれています。

npm install @tauri-apps/api
# or
pnpm add @tauri-apps/api
# or
yarn add @tauri-apps/api

グローバルイベントのリッスン

基本的なリッスンのパターン
import { listen } from '@tauri-apps/api/event';

// Listen for a global event
const unlisten = await listen('download-started', (event) => {
  console.log('Event received:', event.payload);
});

// Clean up when done
unlisten();
型付きイベントペイロード

Rust のペイロード構造体に対応する TypeScript インターフェースを定義します。

import { listen } from '@tauri-apps/api/event';

// Define the payload type (matches Rust struct with camelCase)
interface DownloadStarted {
  url: string;
  downloadId: number;
  contentLength: number;
}

// Use generic type parameter for type safety
const unlisten = await listen<DownloadStarted>('download-started', (event) => {
  console.log(`Downloading from ${event.payload.url}`);
  console.log(`Content length: ${event.payload.contentLength} bytes`);
  console.log(`Download ID: ${event.payload.downloadId}`);
});
イベントオブジェクトの構造

イベントコールバックは Event<T> オブジェクトを受け取ります。

interface Event<T> {
  /** Event name */
  event: string;
  /** Event identifier */
  id: number;
  /** Event payload (your custom type) */
  payload: T;
}

WebView 固有のイベントのリッスン

特定の WebView を対象とする emit_to または emit_filter で発行されたイベントには、次を使用します。

import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';

const appWebview = getCurrentWebviewWindow();

// Listen for events targeted to this specific webview
const unlisten = await appWebview.listen<string>('login-result', (event) => {
  if (event.payload === 'loggedIn') {
    localStorage.setItem('authenticated', 'true');
  } else {
    console.error('Login failed:', event.payload);
  }
});

一度だけのイベントリスナー

一度だけ処理するイベントには once を使用します。

import { once } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';

// Global one-time listener
await once('app-ready', (event) => {
  console.log('Application is ready');
});

// Webview-specific one-time listener
const appWebview = getCurrentWebviewWindow();
await appWebview.once('initialized', (event) => {
  console.log('Webview initialized');
});

クリーンアップとサブスクリプション解除

手動クリーンアップ

不要になったリスナーは、必ずサブスクリプションを解除してください。

import { listen } from '@tauri-apps/api/event';

const unlisten = await listen('my-event', (event) => {
  // Handle event
});

// Later, when done listening
unlisten();
React コンポーネントのクリーンアップ
import { useEffect } from 'react';
import { listen } from '@tauri-apps/api/event';

interface ProgressPayload {
  percent: number;
  message: string;
}

function DownloadProgress() {
  useEffect(() => {
    let unlisten: (() => void) | undefined;

    const setupListener = async () => {
      unlisten = await listen<ProgressPayload>('download-progress', (event) => {
        console.log(`Progress: ${event.payload.percent}%`);
      });
    };

    setupListener();

    // Cleanup when component unmounts
    return () => {
      if (unlisten) {
        unlisten();
      }
    };
  }, []);

  return <div>Listening for progress...</div>;
}
Vue Composition API のクリーンアップ
import { onMounted, onUnmounted } from 'vue';
import { listen } from '@tauri-apps/api/event';

interface NotificationPayload {
  title: string;
  body: string;
}

export default {
  setup() {
    let unlisten: (() => void) | undefined;

    onMounted(async () => {
      unlisten = await listen<NotificationPayload>('notification', (event) => {
        console.log(`${event.payload.title}: ${event.payload.body}`);
      });
    });

    onUnmounted(() => {
      if (unlisten) {
        unlisten();
      }
    });
  }
};
Svelte のクリーンアップ
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { listen } from '@tauri-apps/api/event';

  interface StatusPayload {
    status: 'idle' | 'loading' | 'complete';
  }

  let unlisten: (() => void) | undefined;

  onMount(async () => {
    unlisten = await listen<StatusPayload>('status-change', (event) => {
      console.log('Status:', event.payload.status);
    });
  });

  onDestroy(() => {
    if (unlisten) {
      unlisten();
    }
  });
</script>

リスナーの自動クリーンアップ

Tauri は次の状況でリスナーを自動的にクリアします。

  • ページの再読み込み - すべてのリスナーがクリアされます
  • URL ナビゲーション - リスナーがクリアされます(SPA ルーターを除く)

警告: ページ全体の再読み込みを発生させないシングルページアプリケーションのルーターでは、リスナーは自動的にクリーンアップされません。手動でサブスクリプションを解除する必要があります。

複数のイベントリスナー

複数のイベントをリッスンする
import { listen } from '@tauri-apps/api/event';

interface DownloadEvent {
  url: string;
}

interface ProgressEvent {
  percent: number;
}

interface CompleteEvent {
  path: string;
  size: number;
}

async function setupDownloadListeners() {
  const unlisteners: Array<() => void> = [];

  unlisteners.push(
    await listen<DownloadEvent>('download-started', (event) => {
      console.log(`Started downloading: ${event.payload.url}`);
    })
  );

  unlisteners.push(
    await listen<ProgressEvent>('download-progress', (event) => {
      console.log(`Progress: ${event.payload.percent}%`);
    })
  );

  unlisteners.push(
    await listen<CompleteEvent>('download-complete', (event) => {
      console.log(`Complete: ${event.payload.path} (${event.payload.size} bytes)`);
    })
  );

  // Return cleanup function
  return () => {
    unlisteners.forEach((unlisten) => unlisten());
  };
}

// Usage
const cleanup = await setupDownloadListeners();
// Later...
cleanup();

型定義のリファレンス

型付きイベントヘルパーの作成
import { listen, once, Event } from '@tauri-apps/api/event';

// Define all your event types in one place
export interface AppEvents {
  'download-started': { url: string; downloadId: number };
  'download-progress': { downloadId: number; percent: number };
  'download-complete': { downloadId: number; path: string };
  'download-error': { downloadId: number; error: string };
  'notification': { title: string; body: string };
}

// Type-safe listen helper
export async function listenTyped<K extends keyof AppEvents>(
  eventName: K,
  handler: (event: Event<AppEvents[K]>) => void
): Promise<() => void> {
  return listen<AppEvents[K]>(eventName, handler);
}

// Usage
const unlisten = await listenTyped('download-started', (event) => {
  // event.payload is typed as { url: string; downloadId: number }
  console.log(event.payload.url);
});

対応する 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,
}

#[tauri::command]
fn start_download(app: AppHandle, url: String) {
    app.emit("download-started", DownloadStarted {
        url,
        download_id: 1,
        content_length: 1024,
    }).unwrap();
}
WebView 固有のイベントの発行
use tauri::{AppHandle, Emitter};

#[tauri::command]
fn notify_window(app: AppHandle, window_label: &str, message: String) {
    // Emit to a specific webview by label
    app.emit_to(window_label, "notification", message).unwrap();
}
フィルター付きイベントの発行
use tauri::{AppHandle, Emitter, EventTarget};

#[tauri::command]
fn broadcast_to_editors(app: AppHandle, content: String) {
    app.emit_filter("content-update", content, |target| {
        match target {
            EventTarget::WebviewWindow { label } => {
                label.starts_with("editor-")
            }
            _ => false,
        }
    }).unwrap();
}

ベストプラクティス

  1. コンパイル時に型の不一致を検出できるよう、イベントペイロードには必ず TypeScript インターフェースを定義してください

  2. JavaScript の命名規則に合わせるため、Rust の構造体では serde(rename_all = "camelCase") を使用してください

  3. メモリーリークを防ぐため、サブスクリプション解除関数を保存し、クリーンアップ時に呼び出してください

  4. 一度だけ発生する初期化イベントには once を使用してください

  5. リスナーのスコープをイベント発行のスコープに合わせてください。emit_to イベントには WebView 固有のリスナーを使用してください

  6. 一貫性を保つため、イベントの型定義を共有モジュールに集約してください

よくあるパターン

イベントバスパターン
import { listen, Event } from '@tauri-apps/api/event';

type EventHandler<T> = (payload: T) => void;

class TauriEventBus {
  private unlisteners: Map<string, () => void> = new Map();

  async subscribe<T>(event: string, handler: EventHandler<T>): Promise<void> {
    if (this.unlisteners.has(event)) {
      this.unlisteners.get(event)!();
    }

    const unlisten = await listen<T>(event, (e: Event<T>) => {
      handler(e.payload);
    });

    this.unlisteners.set(event, unlisten);
  }

  unsubscribe(event: string): void {
    const unlisten = this.unlisteners.get(event);
    if (unlisten) {
      unlisten();
      this.unlisteners.delete(event);
    }
  }

  unsubscribeAll(): void {
    this.unlisteners.forEach((unlisten) => unlisten());
    this.unlisteners.clear();
  }
}

export const eventBus = new TauriEventBus();
React フックパターン
import { useEffect, useState } from 'react';
import { listen, Event } from '@tauri-apps/api/event';

export function useTauriEvent<T>(eventName: string, initialValue: T): T {
  const [value, setValue] = useState<T>(initialValue);

  useEffect(() => {
    let unlisten: (() => void) | undefined;

    listen<T>(eventName, (event: Event<T>) => {
      setValue(event.payload);
    }).then((fn) => {
      unlisten = fn;
    });

    return () => {
      if (unlisten) {
        unlisten();
      }
    };
  }, [eventName]);

  return value;
}

// Usage
function StatusDisplay() {
  const status = useTauriEvent<string>('app-status', 'initializing');
  return <div>Status: {status}</div>;
}

コメント