calling-frontend-from-tauri-rust について
Tauri 開発者向けに、Rust とフロントエンド間の通信方法を解説するスキルです。イベント、チャネル、JavaScript 評価の例を示し、用途に応じた選び方を説明します。
主な機能
- 01イベントのブロードキャストと宛先指定
- 02イベントリスナーの管理
- 03チャネルによるデータストリーミング
- 04Rust からの JavaScript 実行
- 05用途別の方式比較
活用シーン
- 01複数のフロントエンドウィンドウへの通知
- 02高頻度なダウンロード進捗の報告
- 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 Eval | DOM の直接操作、応答不要 | オーバーヘッドが小さい |
イベント:複数のウィンドウへの通知、疎結合、シンプルなステータス更新。
チャネル:進捗状況を伴うファイルのダウンロード/アップロード、リアルタイムストリーミング、高頻度の更新。
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();
コメント
Markdown 対応