Commands vs Events
Tauri 提供两种前后端通信机制,适用于不同场景:
Commands(invoke)
前端主动调用 Rust,请求-响应模式(类似函数调用)。适合:查询数据、执行操作并等待结果。特点:有明确的返回值,支持错误处理。
Events(emit/listen)
任意一方可以主动推送消息给另一方,发布-订阅模式。适合:进度更新、状态变化通知、后台任务完成通知、多窗口广播。特点:异步推送,无需等待。
前端 → Rust:前端发送事件
import { emit, emitTo } from '@tauri-apps/api/event';
// 向所有监听者广播事件(前端和 Rust 都能收到)
await emit('user-action', {
type: 'click',
target: 'button-save',
timestamp: Date.now(),
});
// 发送给特定窗口
await emitTo('settings-window', 'config-changed', {
theme: 'dark',
});
Rust → 前端:Rust 推送事件
use tauri::{AppHandle, Manager, Emitter};
// 在 Command 中获取 AppHandle 并发送事件
#[tauri::command]
async fn start_download(app: AppHandle, url: String) -> Result<(), String> {
// 克隆 handle,在 spawn 中使用
let app_clone = app.clone();
tokio::task::spawn(async move {
for progress in 0..=100 {
tokio::time::sleep(Duration::from_millis(50)).await;
// 向前端发送进度事件
app_clone.emit("download-progress", serde_json::json!({
"progress": progress,
"url": &url,
"done": progress == 100,
})).unwrap();
}
});
Ok(())
}
前端监听事件
import { listen, once } from '@tauri-apps/api/event';
// 持续监听事件(返回取消监听的函数)
const unlisten = await listen<{ progress: number; done: boolean }>(
'download-progress',
(event) => {
console.log('Progress:', event.payload.progress);
if (event.payload.done) {
console.log('Download complete!');
unlisten(); // 完成后取消监听
}
}
);
// 只监听一次(自动取消)
const payload = await new Promise(resolve => {
once('task-complete', (event) => resolve(event.payload));
});
进度反馈完整示例
Rust 端:文件压缩并报告进度
#[derive(serde::Serialize, Clone)]
struct CompressProgress {
file: String,
current: usize,
total: usize,
percent: f64,
}
#[tauri::command]
async fn compress_files(
app: AppHandle,
files: Vec<String>,
output: String,
) -> Result<(), String> {
let total = files.len();
for (i, file) in files.iter().enumerate() {
// 执行压缩逻辑(省略)
compress_one_file(file, &output)?;
// 发送进度
app.emit("compress-progress", CompressProgress {
file: file.clone(),
current: i + 1,
total,
percent: (i + 1) as f64 / total as f64 * 100.0,
}).map_err(|e| e.to_string())?;
}
// 完成事件
app.emit("compress-done", &output).unwrap();
Ok(())
}
前端 React 组件
import { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
interface Progress {
file: string;
current: number;
total: number;
percent: number;
}
function Compressor() {
const [progress, setProgress] = useState<Progress | null>(null);
const [done, setDone] = useState(false);
useEffect(() => {
let unlistenProgress: (() => void) | undefined;
let unlistenDone: (() => void) | undefined;
(async () => {
// 注册监听器
unlistenProgress = await listen<Progress>(
'compress-progress',
(e) => setProgress(e.payload)
);
unlistenDone = await listen('compress-done', () => setDone(true));
})();
// 组件卸载时取消监听(防止内存泄漏)
return () => {
unlistenProgress?.();
unlistenDone?.();
};
}, []);
async function startCompress() {
setDone(false);
await invoke('compress_files', {
files: ['/tmp/a.txt', '/tmp/b.txt'],
output: '/tmp/archive.zip',
});
}
return (
<div>
<button onClick={startCompress}>开始压缩</button>
{progress && (
<div>
<p>{progress.file} ({progress.current}/{progress.total})</p>
<progress value={progress.percent} max="100" />
</div>
)}
{done && <p>压缩完成!</p>}
</div>
);
}
窗口特定事件
use tauri::{Manager, WindowEvent};
// 监听特定窗口的系统事件
pub fn run() {
tauri::Builder::default()
.on_window_event(|window, event| {
match event {
WindowEvent::CloseRequested { api, .. } => {
// 拦截关闭请求,显示确认对话框
api.prevent_close();
let window = window.clone();
tauri::async_runtime::spawn(async move {
// 发事件给前端,让前端显示确认弹窗
window.emit("close-requested", ()).unwrap();
});
}
WindowEvent::Focused(focused) => {
println!("Window focused: {}", focused);
}
_ => {}
}
})
.run(tauri::generate_context!())
.unwrap();
}
记得取消监听
listen() 返回的取消函数必须在组件卸载时调用,否则会造成内存泄漏——即使组件已销毁,事件回调仍会被触发。在 React 中放在 useEffect 的 cleanup 函数中,在 Vue 中放在 onUnmounted 钩子中。
事件 vs Commands 的选择指南
Commands(invoke)
前端主动调用 Rust 函数,等待返回值。适合:请求-响应模式(查询数据、执行操作)、需要错误处理的操作、一次性操作(打开文件、保存设置)。
Events(emit/listen)
单向通知,可以从任一方向发送,不等待响应。适合:长时间运行的进度更新、Rust 主动推送通知(网络状态、后台任务完成)、多窗口间通信。
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 读取文件内容 | Command (invoke) | 需要返回值,有错误处理 |
| 下载进度更新 | Event (emit from Rust) | 多次单向推送 |
| 用户点击按钮触发操作 | Command (invoke) | 等待操作结果 |
| 系统通知(新消息) | Event (Rust → Frontend) | Rust 主动推送 |
| 多窗口数据同步 | Event (broadcast) | 一对多广播 |
| 获取应用设置 | Command (invoke) | 一次性请求-响应 |
全局事件与窗口事件
Tauri 2.0 中事件分为全局事件(所有窗口可接收)和窗口事件(只有特定窗口接收):
import { emit, listen, emitTo } from '@tauri-apps/api/event';
import { getCurrentWindow } from '@tauri-apps/api/window';
// 全局广播:所有窗口都收到
await emit('settings-changed', { theme: 'dark' });
// 发送给特定窗口
await emitTo('settings', 'theme-update', { theme: 'dark' });
// 发送给当前窗口自己
const currentWindow = getCurrentWindow();
await currentWindow.emit('internal-update', { data: 'value' });
// 监听全局事件
const unlisten = await listen<{ theme: string }>('settings-changed', (event) => {
console.log('新主题:', event.payload.theme);
console.log('来自窗口:', event.windowLabel);
});
// 组件卸载时取消监听
return () => unlisten();
Rust 端监听前端事件
在 Rust 中订阅事件
不只是 Rust 向前端发事件,前端也可以发事件让 Rust 订阅。这在需要持续监控前端状态(如用户活动检测)时很有用:
use tauri::{AppHandle, Manager, Listener};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
// 在 setup 中注册全局事件监听
app.listen("user-action", |event| {
let payload = event.payload();
println!("收到前端事件: {}", payload);
});
// 只监听一次
app.once("app-ready", |_event| {
println!("前端已就绪");
});
Ok(())
})
.run(tauri::generate_context!())
.unwrap();
}
app.listen(event, handler)
注册全局事件监听器,返回 EventId 可用于
app.unlisten(id) 取消。handler 在 Rust 线程中执行,注意不要执行耗时操作(应 spawn 异步任务)。app.once(event, handler)
只监听一次,触发后自动移除。适合等待初始化完成事件。
window.listen(event, handler)
只监听来自特定窗口的事件,比全局监听更精确,避免窗口间事件混淆。
Channel:流式数据传输
Channel 的使用场景
Tauri 2.0 引入了 Channel API,专为从 Rust 向前端流式传输大量数据而设计。与 Event 的区别是:Event 是广播(任何人都能收),Channel 是一对一的直接通道,更高效且有序。
use tauri::{AppHandle, ipc::Channel};
use serde::Serialize;
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase", tag = "event", content = "data")]
enum ProgressEvent {
Started { total: usize },
Progress { current: usize, item: String },
Completed { output_path: String },
Error { message: String },
}
#[tauri::command]
async fn process_files(
files: Vec<String>,
on_progress: Channel<ProgressEvent>, // Channel 直接作为参数接收
) -> Result<(), String> {
let total = files.len();
// 发送开始事件
on_progress.send(ProgressEvent::Started { total })
.map_err(|e| e.to_string())?;
for (i, file) in files.iter().enumerate() {
// 模拟处理
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
on_progress.send(ProgressEvent::Progress {
current: i + 1,
item: file.clone(),
}).map_err(|e| e.to_string())?;
}
on_progress.send(ProgressEvent::Completed {
output_path: "/tmp/result".to_string(),
}).map_err(|e| e.to_string())?;
Ok(())
}
import { invoke } from '@tauri-apps/api/core';
import { Channel } from '@tauri-apps/api/core';
// 创建 Channel 并传给 Rust Command
const channel = new Channel<{
event: 'started' | 'progress' | 'completed' | 'error';
data: any;
}>();
// 注册消息处理器
channel.onmessage = (msg) => {
switch (msg.event) {
case 'started':
console.log(`开始处理 ${msg.data.total} 个文件`);
break;
case 'progress':
console.log(`处理中 [${msg.data.current}]: ${msg.data.item}`);
break;
case 'completed':
console.log(`完成!输出: ${msg.data.outputPath}`);
break;
}
};
// 调用 Command 并传入 Channel
await invoke('process_files', {
files: ['/tmp/a.txt', '/tmp/b.txt'],
onProgress: channel, // camelCase 对应 Rust 的 on_progress
});
Channel vs Event 的选择
Channel 是 Tauri 2.0 新增的 API,专为流式数据传输优化:Channel 适合一次性的流式传输(一个 invoke 对应一个 Channel),数据有序、一对一、高效;Event 适合持续的广播通知(多个订阅者),或 Rust 主动发起的推送(不需要前端先调用 invoke)。进度反馈两种都可以,Channel 更简洁;系统级通知(新消息来了、网络断开)用 Event 更自然。
事件调试技巧
import { listen } from '@tauri-apps/api/event';
// 开发时:监听所有事件(用于调试)
if (import.meta.env.DEV) {
listen('*', (event) => {
console.log(`[Event] ${event.event}`, event.payload);
});
}
// 查看事件元数据
const unlisten = await listen('my-event', (event) => {
console.log('事件名:', event.event);
console.log('来源窗口:', event.windowLabel);
console.log('负载:', event.payload);
});
事件系统的常见陷阱
- 忘记取消监听:
listen()是全局注册,组件卸载后如果没有调用取消函数,事件回调仍然执行,可能导致状态更新到已卸载的组件(React 中会有警告) - 异步监听注册竞争:在组件挂载时用 async IIFE 注册 listen,保存 unlisten 时要注意异步时序;推荐使用 useEffect + cleanup 模式
- Rust listen 的线程安全:Rust 的事件 handler 在 Tauri 事件线程执行,如果要更新 Mutex 状态,需要小心避免死锁;复杂操作应 spawn 异步任务
- 序列化要求:Event payload 必须实现 serde::Serialize(Rust 端)和 JSON 兼容类型(TypeScript 端)
本章小结
本章核心要点
- Events 是异步单向通知:适合 Rust 主动推送状态变更(进度、通知),与 Commands 的请求-响应模式互补;事件是广播(一对多),Channel 是一对一有序流。
- emit/listen 是核心 API:Rust 用
app_handle.emit()或window.emit()发送;前端用listen()订阅,并在组件卸载时调用返回的取消函数防止内存泄漏。 - Channel API(Tauri 2.0 新增):前端创建 Channel 对象传入 invoke,Rust 通过
channel.send()流式发送数据;有序、高效、一对一,适合进度流和结果流。 - Rust 也可以监听事件:通过
app.listen()在 setup 中注册,前端 emit 的事件 Rust 也能接收;app.once()只监听一次后自动取消。 - 全局 vs 窗口事件:
emit()全局广播所有窗口;emitTo(label, ...)发送给特定窗口;window.emit()只发给当前窗口。 - 选择原则:需要返回值 → Command;单向通知/多次推送 → Event;一个任务一个进度流 → Channel;两者结合(invoke 启动任务 + Channel 报告进度)是最佳实践。