事件系统:Rust 主动通知前端
命令解决「前端问、Rust 答」,事件解决「Rust 说、前端听」。后台下载进度、外部设备接入、定时任务完成这类场景,前端不可能一直轮询,事件系统正是为此设计。本篇讲事件的双向用法、订阅管理与典型场景实现。
一、事件的基本用法
Rust 端发送事件使用 app_handle.emit:
use tauri::{AppHandle, Emitter};
#[tauri::command]
async fn start_task(app: AppHandle) {
for i in 1..=10 {
std::thread::sleep(std::time::Duration::from_secs(1));
app.emit("progress", i * 10).unwrap();
}
}
前端订阅:
import { listen } from '@tauri-apps/api/event'
const unlisten = await listen('progress', (event) => {
console.log(`进度:${event.payload}%`)
})
// 不再需要时取消订阅
unlisten()
事件载荷依然是 JSON 序列化的,event.payload 直接是前端可用的数据。注意 listen 返回的是一个取消订阅函数,类型为异步,需要 await。
| 对比项 | invoke 命令 | 事件 |
|---|---|---|
| 方向 | 前端到 Rust | Rust 到前端,也可反向 |
| 触发者 | 前端 | 任意一端 |
| 返回值 | 有,Promise | 无,回调接收 |
| 适用场景 | 请求响应 | 状态推送与通知 |
二、事件的定向发送
全局 emit 会发给所有窗口。如果只想通知某个窗口,可以拿到对应的 WebviewWindow 再 emit:
use tauri::{WebviewWindow, Emitter};
fn notify_window(window: &WebviewWindow) {
window.emit("file-saved", "data.json").unwrap();
}
前端到 Rust 的事件同样支持,Rust 端用 listen 方法订阅,常用作逆向通知场景,例如前端通知 Rust 做一次低频的全量同步。具体 API 以官方文档为准。
三、订阅管理与内存泄漏
事件系统最常见的坑是忘记取消订阅。在单页应用里,组件卸载后监听器仍然存在,重复进入组件会叠加订阅,同一事件被处理多次。Vue 与 React 的规范写法:
import { listen } from '@tauri-apps/api/event'
import { onBeforeUnmount } from 'vue'
const unlisten = await listen('progress', (e) => {
console.log(e.payload)
})
onBeforeUnmount(() => {
unlisten()
})
| 框架 | 取消订阅时机 |
|---|---|
| Vue | onBeforeUnmount |
| React | useEffect 返回清理函数 |
| Svelte | 组件销毁回调 |
养成「注册与取消成对出现」的习惯,调试事件重复触发问题时,先怀疑订阅泄漏。
四、典型场景:带进度的后台任务
把命令与事件组合,就能实现完整的后台任务模式。前端 invoke 启动任务,命令内部异步执行并不断 emit 进度,完成后 emit 结束事件:
use tauri::{AppHandle, Emitter};
#[tauri::command]
async fn download(app: AppHandle, url: String) -> Result<(), String> {
for step in 1..=5 {
std::thread::sleep(std::time::Duration::from_millis(500));
app.emit("download-progress", step * 20).unwrap();
}
app.emit("download-done", ()).unwrap();
Ok(())
}
前端同时维护进度条与完成状态,代码结构比轮询清晰得多。这个模式的完整异步写法见「异步命令与后台任务」,窗口间的状态共享见「全局状态:tauri::State 与共享数据」。
五、事件命名与规范
事件多了之后需要约定命名,建议用「域名加动作」格式,例如 download:progress、sync:complete,并在项目里维护一份事件清单文档。载荷结构也建议统一,比如总是包一层 { status, data },方便前端写通用处理函数。
事件系统的设计与浏览器扩展的消息机制很相似,做过扩展开发的同学可以对照 浏览器扩展指南 理解。Rust 端事件枚举与字符串管理,配合 正则 mastery 做批量重命名也很方便,Rust 语法基础见 Rust 入门。
六、事件系统的可靠投递
事件是「发后不管」的,它不像命令那样有确认机制。设计关键链路时要想清楚丢事件的影响。三种增强手段:
| 手段 | 做法 | 适用 |
|---|---|---|
| 序列号校验 | 载荷带递增序号,前端检测跳号 | 要求顺序的场景 |
| 心跳确认 | 前端定期检查最后事件时间 | 判断后台是否存活 |
| 关键事件补 command 询问 | 事件只做通知,数据用命令拉取 | 必须准确的数据 |
第三种是最常用的模式:事件负责「喊一嗓子」,前端收到后用 invoke 拉完整数据。这样即使事件丢了,下次任何事件触发时数据也会自动修正;而事件重发也不会导致数据重复处理。把通知与数据分离,可靠性问题自然消解大半。
事件系统与命令系统的分工再强调一次:命令用于请求,事件用于推送。设计接口时先问自己「这是前端发起还是 Rust 发起」,答案直接决定用哪一套。常见误用是把事件当命令的返回值用,或者反过来轮询命令模拟事件,两者都说明通信模型没有理清。把这两条通道的设计在文档里写清楚,团队成员读代码会轻松很多。
再补一个协作层面的建议:事件清单与命令清单放在同一份文档里维护,按命名空间分组。新成员接手项目时,这份文档几乎就是全部接口语义。文档与代码不一致是常态而非例外,因此把它纳入 code review 的必查项,虽然繁琐但回报极高——事件系统最怕的就是没有人说得清某个事件是谁发的、谁在收。
再给两条排错经验。其一,事件监听不生效时先检查事件名拼写,再检查订阅代码是否真的执行到,最后检查 Capability 是否声明了事件相关权限。其二,若只有部分窗口收到事件,多半是 emit 作用域问题,全局与定向行为不同,对照本篇第二节确认。事件排错本质沿「发送方、通道、接收方」三段逐段验证,习惯这个流程后定位会很快。
| 排查顺序 | 检查点 |
|---|---|
| 一 | 事件名拼写 |
| 二 | 订阅代码是否执行 |
| 三 | 权限是否声明 |
| 四 | emit 作用域 |
关于事件还有一个设计层面的建议:把事件当作「系统的神经信号」而不是「数据的运输车」。神经信号负责通知变化,具体数据让前端用命令拉取。这个「事件轻、命令重」的分工能让整个应用的数据流保持单向与可预测,调试时只需追两类调用,而不是面对满天飞的事件载荷。它还顺带解决了重复消费与乱序问题:通知可以重复,数据查询天然幂等。把这条原则写进团队的接口规范,半年后回看会感谢此刻的自己。
顺带一句提示:调试事件时可以在 Rust 端订阅一份相同的事件打印日志,两端同事核对,排查「发了没收到」这类问题会快很多。工具虽朴素,效果立竿见影,建议直接固化进项目的调试技巧清单。
延伸阅读
- 异步命令与后台任务——事件推送的生产者实现
- 窗口管理:创建、多窗口与装饰——定向事件的目标
- 浏览器扩展指南:相似消息模型的对照学习
