在 Tauri 应用中,“主进程”指的是由 Rust 编写的后端二进制程序,“渲染进程”则是前端页面所在的 WebView。两者运行在相互隔离的环境中,但 Tauri 提供了一套高效、安全且易于使用的机制来实现双向通信。
前端调用后端:命令(Command)
前端通过 invoke 函数调用 Rust 端用 #[tauri::command] 标注的函数,就像调用一个异步 API 一样。
Rust 端定义一个命令:
#[tauri::command]
fn greet(name: String) -> String {
format!("你好, {}!", name)
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("启动失败");
}
前端调用:
import { invoke } from '@tauri-apps/api/core';
const message = await invoke('greet', { name: 'Tauri' });
console.log(message); // "你好, Tauri!"
invoke返回一个 Promise,异步执行 Rust 函数,不会阻塞 UI。- 参数和返回值会自动序列化/反序列化,支持常见类型甚至自定义结构体。
- 安全边界:命令必须在
invoke_handler中显式注册,前端无法调用未注册的函数。
后端推送事件到前端:事件系统
当 Rust 端需要主动通知前端(例如下载进度、系统托盘点击、后台任务完成),可以使用事件系统。
Rust 端发射事件:
use tauri::Manager;
#[tauri::command]
async fn start_process<R: tauri::Runtime>(app: tauri::AppHandle<R>) {
std::thread::spawn(move || {
// 模拟进度更新
for i in 1..=100 {
std::thread::sleep(std::time::Duration::from_millis(50));
app.emit("progress", i).unwrap();
}
});
}
前端监听事件:
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('progress', (event) => {
console.log('进度:', event.payload);
// 更新 UI 进度条
});
// 当组件销毁时取消监听
// unlisten();
- 事件名可以是任意字符串,事件负载支持序列化。
- 支持全局事件(所有窗口都能收到)和窗口级事件(仅指定窗口接收)。
- 前端也可以发射事件到 Rust 端,通过
emit函数配合 Rust 端的listen_global实现反向通信。
前端发射事件到后端
前端:
import { emit } from '@tauri-apps/api/event';
await emit('frontend-event', { message: '用户点击按钮' });
Rust 端监听:
let app_handle = app.handle();
app_handle.listen_global("frontend-event", |event| {
let payload = event.payload().unwrap();
println!("收到前端事件: {:?}", payload);
});
通信的基石:安全隔离
无论如何双向通信,Tauri 始终维持着一道硬性边界:
- 前端代码运行在 WebView 沙箱内,无法直接操作系统资源;
- 所有交互都必须通过注册的命令或事件,Rust 端有绝对的执行控制权;
- 敏感 API(如文件读写)必须由开发者显式授权,不存在“后门”。
这种设计让双路通信在极具灵活性的同时,也保证了应用不会因前端漏洞(如 XSS)而导致系统级劫持。
一句话总结:Tauri 用 invoke 实现前端对后端的主动调用,用事件流实现后端的主动推送或前端的反向通知,整个通信过程完全基于安全的 IPC 管道,兼顾了开发的便利性与生产环境的安全性。