关于 listening-to-tauri-events
面向 Tauri v2 前端开发者的事件订阅指南,讲解如何接收 Rust 发出的事件。内容涵盖全局与 Webview 定向监听、类型化载荷、一次性订阅和清理。
主要功能
- 01全局事件订阅
- 02Webview 专属监听
- 03类型化事件载荷
- 04一次性监听器
- 05手动清理监听器
- 06Rust 事件发送示例
适用场景
- 01接收 Rust 后端更新
- 02处理特定窗口的事件
- 03在 SPA 组件中移除监听器
- 04为 TypeScript 事件数据指定类型
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ dchuk/ claude-code-tauri-skills --skill listening-to-tauri-events安装后在 Claude Code 中输入/listening-to-tauri-events确认已加载。
详细介绍
在前端监听 Tauri 事件
本技能介绍如何在 Tauri v2 应用中使用 @tauri-apps/api/event 模块订阅由 Rust 发出的事件。
核心概念
Tauri 提供两种事件作用域:
- 全局事件 - 广播给所有 webview 中的所有监听器
- 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 专属事件
对于使用 emit_to 或 emit_filter 定向发送到特定 webview 的事件:
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();
}
最佳实践
-
始终为事件负载定义 TypeScript 接口,以便在编译时发现类型不匹配
-
在 Rust 结构体中使用
serde(rename_all = "camelCase"),以匹配 JavaScript 命名约定 -
保存取消监听函数,并在清理时调用,以防止内存泄漏
-
对于只触发一次的初始化事件,使用
once -
使监听器作用域与发出事件的作用域相匹配 - 对
emit_to事件使用 webview 专属监听器 -
在共享模块中集中定义事件类型,以确保一致性
常见模式
事件总线模式
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 Hook 模式
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>;
}
评论
支持 Markdown