首页 / Tauri 2 入门教程 / 从 Rust 主动调用前端

Tauri 2 入门教程

从 Rust 主动调用前端

本教程共 48 篇 · 第 24 篇 · 更新于 2026-08-09 · 约 8 分钟阅读

TaurieventemitlistenRust推送

本节目标:学完能用 Tauri 的事件(event)让 Rust 主动把数据推到前端,理解它和命令 invoke 的本质区别,并写出能跑的最小示例。

前面的命令都是「前端拉、Rust 给」:前端 invoke 一下,Rust 算完返回。但有些场景反过来更自然——比如后台下载进度、定时刷新、硬件状态变化,这些是 Rust 主动想告诉前端的。这套机制叫「事件(event)」。

命令是「拉」,事件是「推」

先记住这对核心区别,后面就不乱了:

  • 命令(command):前端主动发起(拉),Rust 收到后返回一次结果。一对一,有来有回。
  • 事件(event):Rust 主动广播(推),前端提前挂好监听器等着。一对多,Rust 想发几次就发几次,前端不一定回。

事件没有强类型约束,载荷(payload)必须是 JSON,不支持像命令那样的 capabilities 精细权限。它适合「小批量、流式、多消费者」的场景,比如推送通知、进度条更新。

Note

事件永远是异步的,不能返回值。如果你需要「前端问、Rust 答」的往返,那就该用命令,而不是事件。

Rust 侧用 emit 主动推送

Rust 想发事件,靠的是 Emitter trait 上的 emit 方法。任何持有 AppHandleWebviewWindow 的地方都能用,记得先 use tauri::Emitter;

// src-tauri/src/lib.rs
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();
}

app.emit("事件名", 载荷) 把事件广播给所有监听者。如果你手里有的是 WebviewWindow 而非 AppHandle,调用方式一样:window.emit("事件名", 载荷)

这两者的区别在「范围」:AppHandle 代表整个应用,从它发出的事件默认面向全局;WebviewWindow 代表某个具体窗口,用 window.emit 时语义上更偏向「从这个窗口的视角发出」。无论哪种,要精确控制「只给某窗口」都得用 emit_to。新手常纠结该抓哪个句柄——简单说,命令函数签名里写 app: AppHandle 最通用,几乎什么都能干;只有当你明确需要操作「发起调用的那个窗口」本身(比如改它的标题、隐藏它)时,才用 WebviewWindow

事件载荷可以是任意「可序列化且可克隆(Clone)」的类型。所以推荐用派生了 CloneSerialize 的结构体,传更丰富的字段:

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

#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct DownloadStarted<'a> {
    url: &'a str,
    download_id: usize,
    content_length: usize,
}

#[tauri::command]
fn download(app: AppHandle, url: String) {
    app.emit("download-started", DownloadStarted {
        url: &url,
        download_id: 1,
        content_length: 1000,
    }).unwrap();
}
Warning

事件载荷必须同时满足 SerializeClone。忘加 #[derive(Clone)] 是新手常见编译错误,记住这两个派生要一起写。

前端用 listen 接收事件

前端用 @tauri-apps/api/event 里的 listen 挂监听器。回调函数收到的 event 对象,真正的数据在 event.payload 上。

import { listen } from '@tauri-apps/api/event';

type ProgressPayload = number;

listen<ProgressPayload>('download-progress', (event) => {
  console.log('当前进度:', event.payload);
});

listen 本身返回的是一个取消函数(Promise 解析为 unlisten)。当这个组件要卸载时,调用它能避免内存泄漏:

const unlisten = await listen<number>('download-progress', (e) => {
  console.log(e.payload);
});
// 之后想停止监听:
unlisten();

全局广播 vs 推给指定窗口

app.emit 是全局的:所有监听了该事件名的前端都会收到。如果你只希望某个窗口收到,用 emit_to,指定窗口的 label:

// 只推给 label 为 "editor" 的窗口
app.emit_to("editor", "file-changed", payload).unwrap();

还有更灵活的 emit_filter,可以写一个闭包决定推给哪些窗口。反过来,前端也能用 emitTo(来自 @tauri-apps/api/event)从某个窗口向特定窗口发事件。

Tip

全局事件会送达所有监听器;而「定向事件」不会触发普通全局监听。如果你在前端想接收「任意目标」的事件(catch-all),要用 listen_any 而不是 listen

完整最小示例(前后端打通)

Rust 侧:一个命令被调用后,在后台循环推送进度(这里借用上一章的异步运行时技巧):

// src-tauri/src/lib.rs
use std::time::Duration;
use tauri::{AppHandle, Emitter};
use tauri::async_runtime;

#[tauri::command]
fn start_progress(app: AppHandle) {
    async_runtime::spawn(async move {
        for i in 0..=100 {
            app.emit("progress", i).ok();
            async_runtime::sleep(Duration::from_millis(30)).await;
        }
    });
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![start_progress])
        .run(tauri::generate_context!())
        .expect("启动 Tauri 应用失败");
}

前端:点按钮启动命令,同时挂好监听器把进度显示出来:

import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';

// 先挂监听,再触发命令,避免漏掉最早几条事件
const unlisten = await listen<number>('progress', (event) => {
  console.log('进度:', event.payload, '%');
});

await invoke('start_progress');

// 不再需要时:
// unlisten();
Warning

注意顺序:应该listeninvoke。否则命令瞬间发出的前几条事件,可能早于监听器挂好,就被丢了。这个坑在「一调用命令就立刻连发事件」的场景里特别容易踩,养成「先订阅、后触发」的习惯能省掉很多诡异的调试时间。

事件 vs 通道:什么时候用哪个

Tauri 还提供另一种「Rust 推前端」的机制叫通道(channel),它和事件长得像,定位却不同。一句话区分:事件适合「小数据、偶尔推、广播给多个监听者」;通道适合「大数据、高频流、有序送达」

事件底层本质上是在触发时直接执行一段 JavaScript,所以每发一条都有一点开销;要流式推送大文件、WebSocket 消息、子进程输出这类高频大数据,用通道(前端 new Channel() 当参数传给命令,Rust 侧 Channel::send)更高效也更稳。本章聚焦事件,因为它覆盖了绝大多数「通知、进度、状态变化」的日常需求,理解事件后,通道只是换了个 API 的事。

小结

当你需要「Rust 主动、反复、一对多」地通知前端时,用事件:app.emit / window.emit 在 Rust 侧推送,listen 在前端接收。它和命令是互补的两套通信方式——命令是前端拉、事件是 Rust 推。把这两章合起来,前后端双向通信的拼图就完整了。