mcpField
C
calling-frontend-from-tauri-rust

calling-frontend-from-tauri-rust

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

calling-frontend-from-tauri-rust について

Tauri 開発者向けに、Rust とフロントエンド間の通信方法を解説するスキルです。イベント、チャネル、JavaScript 評価の例を示し、用途に応じた選び方を説明します。

主な機能

  1. 01イベントのブロードキャストと宛先指定
  2. 02イベントリスナーの管理
  3. 03チャネルによるデータストリーミング
  4. 04Rust からの JavaScript 実行
  5. 05用途別の方式比較

活用シーン

  1. 01複数のフロントエンドウィンドウへの通知
  2. 02高頻度なダウンロード進捗の報告
  3. 03単発のフロントエンド更新の実行

インストール

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

$npx skills add https://github.com/dchuk/claude-code-tauri-skills --skill calling-frontend-from-tauri-rust

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

詳細

Tauri の Rust からフロントエンドを呼び出す

Tauri では、Rust からフロントエンドと通信するために、イベントシステム、チャネル、JavaScript の評価という 3 つの仕組みを利用できます。

イベントシステムの概要

イベントシステムを使用すると、Rust とフロントエンド間で双方向通信できます。少量のデータ転送や、複数のコンシューマーがあるパターンに適しています。低レイテンシーや高スループット向けには設計されていません。

必要なインポート
use tauri::{AppHandle, Emitter, Manager, Listener, EventTarget};
use serde::Serialize;
import { listen, once, emit } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';

Rust からイベントを送信する

グローバルイベント(すべてのリスナー)

AppHandle::emit() を使用して、すべてのリスナーにブロードキャストします。

use tauri::{AppHandle, Emitter};

#[tauri::command]
fn download(app: AppHandle, url: String) {
    app.emit("download-started", &url).unwrap();
    for progress in [1, 15, 50, 80, 100] {
        app.emit("download-progress", progress).unwrap();
    }
    app.emit("download-finished", &url).unwrap();
}
Webview 固有のイベント

emit_to() で特定の Webview を対象にします。

use tauri::{AppHandle, Emitter};

#[tauri::command]
fn login(app: AppHandle, user: String, password: String) {
    let authenticated = user == "tauri-apps" && password == "tauri";
    let result = if authenticated { "loggedIn" } else { "invalidCredentials" };
    app.emit_to("login", "login-result", result).unwrap();
}
フィルタリングされたイベント(複数の Webview)

emit_filter() を使用して、条件に基づいて対象を指定します。

use tauri::{AppHandle, Emitter, EventTarget};

#[tauri::command]
fn open_file(app: AppHandle, path: std::path::PathBuf) {
    app.emit_filter("open-file", path, |target| match target {
        EventTarget::WebviewWindow { label } => label == "main" || label == "file-viewer",
        _ => false,
    }).unwrap();
}

イベントのペイロード

カスタムペイロードは Serialize と Clone を実装する必要があります。

use serde::Serialize;
use tauri::{AppHandle, Emitter};

#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct DownloadProgress {
    download_id: usize,
    chunk_length: usize,
    total_size: usize,
}

#[tauri::command]
fn download(app: AppHandle, url: String) {
    app.emit("download-progress", DownloadProgress {
        download_id: 1,
        chunk_length: 150,
        total_size: 1000,
    }).unwrap();
}

フロントエンドでリッスンする

グローバルイベントリスナー
import { listen } from '@tauri-apps/api/event';

type DownloadStarted = {
    url: string;
    downloadId: number;
    contentLength: number;
};

listen<DownloadStarted>('download-started', (event) => {
    console.log(`downloading ${event.payload.contentLength} bytes from ${event.payload.url}`);
});
Webview 固有のリスナー
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';

const appWebview = getCurrentWebviewWindow();
appWebview.listen<string>('logged-in', (event) => {
    localStorage.setItem('session-token', event.payload);
});
リスナーの管理
import { listen, once } from '@tauri-apps/api/event';

// Unlisten to prevent memory leaks
const unlisten = await listen('download-started', (event) => {
    console.log('download started');
});
unlisten(); // Stop listening when done

// Listen once for one-time events
once('app-ready', (event) => {
    console.log('App is ready:', event.payload);
});

Rust でリッスンする

グローバルおよび Webview リスナー
use tauri::{Listener, Manager};

tauri::Builder::default()
    .setup(|app| {
        // Global listener
        app.listen("download-started", |event| {
            println!("event received: {}", event.payload());
        });

        // Webview-specific listener
        let webview = app.get_webview_window("main").unwrap();
        webview.listen("logged-in", |event| {
            println!("User logged in: {}", event.payload());
        });
        Ok(())
    })
    .run(tauri::generate_context!())
    .expect("error while running tauri application")
リッスン解除と一度だけリッスンする方法
use tauri::Listener;

// Store event ID to unlisten later
let event_id = app.listen("download-started", |event| {
    println!("download started");
});
app.unlisten(event_id);

// Conditional unlisten
let handle = app.handle().clone();
app.listen("status-changed", move |event| {
    if event.payload() == "\"ready\"" {
        handle.unlisten(event.id());
    }
});

// Listen once
app.once("ready", |event| {
    println!("app is ready: {}", event.payload());
});

チャネル(高スループットのストリーミング)

イベントよりも優れたパフォーマンスを得るには、チャネルを使用します。

Rust でのチャネル設定
use tauri::{AppHandle, ipc::Channel};
use serde::Serialize;

#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase", tag = "event", content = "data")]
enum DownloadEvent<'a> {
    #[serde(rename_all = "camelCase")]
    Started { url: &'a str, download_id: usize, content_length: usize },
    #[serde(rename_all = "camelCase")]
    Progress { download_id: usize, chunk_length: usize },
    #[serde(rename_all = "camelCase")]
    Finished { download_id: usize },
}

#[tauri::command]
fn download(app: AppHandle, url: String, on_event: Channel<DownloadEvent>) {
    on_event.send(DownloadEvent::Started {
        url: &url,
        download_id: 1,
        content_length: 1000,
    }).unwrap();

    for _ in 0..10 {
        on_event.send(DownloadEvent::Progress {
            download_id: 1,
            chunk_length: 100,
        }).unwrap();
    }

    on_event.send(DownloadEvent::Finished { download_id: 1 }).unwrap();
}
フロントエンドでのチャネルの使用
import { invoke, Channel } from '@tauri-apps/api/core';

type DownloadEvent =
    | { event: 'started'; data: { url: string; downloadId: number; contentLength: number } }
    | { event: 'progress'; data: { downloadId: number; chunkLength: number } }
    | { event: 'finished'; data: { downloadId: number } };

const onEvent = new Channel<DownloadEvent>();

onEvent.onmessage = (message) => {
    switch (message.event) {
        case 'started':
            console.log(`Download started: ${message.data.url}`);
            break;
        case 'progress':
            console.log(`Progress: ${message.data.chunkLength} bytes`);
            break;
        case 'finished':
            console.log('Download complete!');
            break;
    }
};

await invoke('download', { url: 'https://example.com/file.json', onEvent });

JavaScript の評価

Rust から JavaScript を直接実行します。

基本的な評価
use tauri::Manager;

tauri::Builder::default()
    .setup(|app| {
        let webview = app.get_webview_window("main").unwrap();
        webview.eval("console.log('hello from Rust')")?;
        Ok(())
    })
データを使用した評価
use tauri::Manager;

#[tauri::command]
fn notify_frontend(app: tauri::AppHandle, message: String) {
    if let Some(webview) = app.get_webview_window("main") {
        let script = format!("window.showNotification('{}')", message);
        webview.eval(&script).unwrap();
    }
}
serialize-to-javascript を使用した複雑なデータ
# Cargo.toml
[dependencies]
serialize-to-javascript = "0.1"
use serialize_to_javascript::Serialized;
use tauri::Manager;

#[derive(serde::Serialize)]
struct AppState { user: String, logged_in: bool }

#[tauri::command]
fn sync_state(app: tauri::AppHandle) {
    let state = AppState { user: "john".to_string(), logged_in: true };
    if let Some(webview) = app.get_webview_window("main") {
        let serialized = Serialized::new(&state, &Default::default()).into_string();
        webview.eval(&format!("window.updateState({})", serialized)).unwrap();
    }
}

適切な方法の選択

方法用途パフォーマンス
イベント(emit)複数のコンシューマー、ブロードキャスト中程度
チャネル高スループットのストリーミング、単一のコンシューマー高い
JS EvalDOM の直接操作、応答不要オーバーヘッドが小さい

イベント:複数のウィンドウへの通知、疎結合、シンプルなステータス更新。

チャネル:進捗状況を伴うファイルのダウンロード/アップロード、リアルタイムストリーミング、高頻度の更新。

JS Eval:単発の DOM 更新、フロントエンド関数の直接呼び出し。

完全な例:ファイルウォッチャー

Rust 側
use tauri::{AppHandle, Emitter};
use serde::Serialize;
use std::path::PathBuf;

#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct FileChange { path: String, event_type: String }

#[tauri::command]
fn watch_directory(app: AppHandle, path: PathBuf) {
    std::thread::spawn(move || {
        loop {
            app.emit("file-changed", FileChange {
                path: path.to_string_lossy().to_string(),
                event_type: "modified".to_string(),
            }).unwrap();
            std::thread::sleep(std::time::Duration::from_secs(5));
        }
    });
}
フロントエンド側
import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/core';

type FileChange = { path: string; eventType: string };

await invoke('watch_directory', { path: '/some/directory' });

const unlisten = await listen<FileChange>('file-changed', (event) => {
    console.log(`File ${event.payload.eventType}: ${event.payload.path}`);
});

// Cleanup when component unmounts: unlisten();

コメント