关于 calling-frontend-from-tauri-rust
面向 Tauri 开发者的实用技能,介绍 Rust 与前端代码的通信方式。通过示例讲解事件、通道和 JavaScript 求值,并说明各自适用场景。
主要功能
- 01广播及定向发送事件
- 02管理事件监听器
- 03通过通道流式传输数据
- 04从 Rust 执行 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 求值。
事件系统概览
事件系统支持 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