进程间通信
Tauri 的前后端通过 IPC(Inter-Process Communication,进程间通信)进行通信。Tauri 提供了两种主要的通信机制:Commands 和 Events。
通信机制对比
| 特性 | Command | Event |
|---|---|---|
| 调用方向 | 前端 → 后端 | 双向 |
| 返回值 | 支持 | 不支持 |
| 类型安全 | 是(编译时检查) | 否(运行时解析) |
| 适用场景 | 请求-响应模式 | 通知、广播、流数据 |
| 底层协议 | JSON-RPC | 消息传递 |
Commands(命令)
Commands 是前端调用后端 Rust 函数的主要方式,采用请求-响应模式。
基础示例
Rust 后端(src-tauri/src/lib.rs):
// 使用 #[tauri::command] 宏标记函数
#[tauri::command]
fn greet(name: &str) -> String {
format!("你好,{}!这是来自 Rust 的问候。", name)
}
pub fn run() {
tauri::Builder::default()
// 注册命令
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
前端调用(JavaScript/TypeScript):
import { invoke } from "@tauri-apps/api/core";
// 调用 greet 命令
async function sayHello() {
const response = await invoke("greet", { name: "世界" });
console.log(response); // 输出: 你好,世界!这是来自 Rust 的问候。
}
sayHello();
参数传递
Command 支持多种参数类型,只要实现了 serde::Deserialize 即可。
基础类型:
#[tauri::command]
fn calculate(a: i32, b: i32, operation: String) -> Result<i32, String> {
match operation.as_str() {
"add" => Ok(a + b),
"subtract" => Ok(a - b),
"multiply" => Ok(a * b),
"divide" => {
if b == 0 {
Err("除数不能为零".to_string())
} else {
Ok(a / b)
}
}
_ => Err("未知操作".to_string()),
}
}
const result = await invoke("calculate", {
a: 10,
b: 20,
operation: "add"
});
结构体参数:
use serde::Deserialize;
#[derive(Deserialize)]
struct User {
name: String,
age: u32,
email: String,
}
#[tauri::command]
fn create_user(user: User) -> String {
format!("创建用户: {} ({}岁)", user.name, user.age)
}
const message = await invoke("create_user", {
user: {
name: "张三",
age: 25,
email: "[email protected]"
}
});
返回值
Command 可以返回任何实现了 serde::Serialize 的类型。
返回结构体:
use serde::Serialize;
#[derive(Serialize)]
struct UserInfo {
id: u64,
name: String,
is_active: bool,
}
#[tauri::command]
fn get_user(id: u64) -> UserInfo {
UserInfo {
id,
name: "张三".to_string(),
is_active: true,
}
}
interface UserInfo {
id: number;
name: string;
isActive: boolean;
}
const user = await invoke<UserInfo>("get_user", { id: 1 });
console.log(user.name); // 张三
错误处理
Command 可以通过 Result 类型返回错误。
use std::fs;
#[tauri::command]
fn read_file(path: String) -> Result<String, String> {
fs::read_to_string(&path)
.map_err(|e| format!("读取文件失败: {}", e))
}
try {
const content = await invoke("read_file", { path: "/path/to/file.txt" });
console.log(content);
} catch (error) {
console.error("错误:", error); // 读取文件失败: No such file or directory
}
异步 Commands
对于耗时操作,使用异步 Command 避免阻塞 UI。
use tokio::time::{sleep, Duration};
#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
// 模拟网络请求
sleep(Duration::from_secs(2)).await;
// 实际使用 reqwest 等 HTTP 客户端
// let response = reqwest::get(&url).await?;
Ok(format!("从 {} 获取的数据", url))
}
// 异步调用不会阻塞 UI
const data = await invoke("fetch_data", {
url: "https://api.example.com/data"
});
访问 Tauri 上下文
Command 可以访问应用状态和窗口实例。
访问窗口:
#[tauri::command]
async fn close_window(window: tauri::WebviewWindow) {
window.close().unwrap();
}
访问应用句柄:
#[tauri::command]
async fn get_app_dir(app_handle: tauri::AppHandle) -> Result<String, String> {
let app_dir = app_handle.path().app_dir()
.map_err(|e| e.to_string())?;
Ok(app_dir.to_string_lossy().to_string())
}
访问全局状态:
use std::sync::Mutex;
struct AppState {
counter: Mutex<i32>,
}
#[tauri::command]
fn increment_counter(state: tauri::State<AppState>) -> i32 {
let mut counter = state.counter.lock().unwrap();
*counter += 1;
*counter
}
// 在 main 函数中初始化状态
.manage(AppState { counter: Mutex::new(0) })
Events(事件)
Events 提供了一种松散的、发布-订阅模式的通信机制,支持双向通信。
前端发送事件
import { emit } from "@tauri-apps/api/event";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
// 发送全局事件
await emit("user-logged-in", { userId: 123, username: "张三" });
// 发送窗口特定事件
const appWindow = getCurrentWebviewWindow();
await appWindow.emit("page-loaded", { timestamp: Date.now() });
// 向特定窗口发送事件
import { emitTo } from "@tauri-apps/api/event";
await emitTo("settings", "theme-changed", { theme: "dark" });
前端监听事件
import { listen } from "@tauri-apps/api/event";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
// 监听全局事件
const unlisten = await listen("download-progress", (event) => {
console.log(`下载进度: ${event.payload.percent}%`);
});
// 监听窗口特定事件
const appWindow = getCurrentWebviewWindow();
appWindow.listen("notification", (event) => {
showNotification(event.payload.message);
});
// 只监听一次
import { once } from "@tauri-apps/api/event";
once("app-ready", () => {
console.log("应用准备就绪");
});
// 取消监听
unlisten();
后端发送事件
use tauri::{AppHandle, Emitter};
// 发送全局事件
#[tauri::command]
fn notify_progress(app: AppHandle, progress: u32) {
app.emit("download-progress", ProgressEvent { percent: progress }).unwrap();
}
// 向特定窗口发送事件
#[tauri::command]
fn notify_window(app: AppHandle, window_label: String, message: String) {
app.emit_to(&window_label, "notification", message).unwrap();
}
// 向多个窗口发送事件
use tauri::EventTarget;
#[tauri::command]
fn broadcast_to_some(app: AppHandle, data: serde_json::Value) {
app.emit_filter("update", data, |target| {
matches!(target, EventTarget::WebviewWindow { label }
if label == "main" || label == "dashboard")
}).unwrap();
}
后端监听事件
use tauri::{Listener, Manager};
pub fn run() {
tauri::Builder::default()
.setup(|app| {
// 监听全局事件
app.listen("frontend-ready", |event| {
println!("前端已就绪: {:?}", event.payload());
});
// 监听特定窗口事件
let main_window = app.get_webview_window("main").unwrap();
main_window.listen("form-submitted", |event| {
println!("表单提交: {:?}", event.payload());
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Channels(通道)
对于需要流式传输大量数据的场景,Tauri 提供了 Channel 机制。
使用场景
- 文件上传/下载进度
- 实时日志输出
- 大文件分块传输
示例:文件下载进度
Rust 后端:
use tauri::ipc::Channel;
use serde::Serialize;
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase", tag = "event", content = "data")]
enum DownloadEvent {
Started { url: String, total_size: u64 },
Progress { downloaded: u64, total: u64 },
Completed { path: String },
Error { message: String },
}
#[tauri::command]
async fn download_file(
url: String,
save_path: String,
on_event: Channel<DownloadEvent>,
) -> Result<(), String> {
// 通知开始下载
on_event.send(DownloadEvent::Started {
url: url.clone(),
total_size: 1024000, // 示例大小
}).map_err(|e| e.to_string())?;
// 模拟下载过程
for i in 1..=10 {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
on_event.send(DownloadEvent::Progress {
downloaded: i * 102400,
total: 1024000,
}).map_err(|e| e.to_string())?;
}
// 通知完成
on_event.send(DownloadEvent::Completed { path: save_path })
.map_err(|e| e.to_string())?;
Ok(())
}
前端代码:
import { invoke, Channel } from "@tauri-apps/api/core";
type DownloadEvent =
| { event: "started"; data: { url: string; totalSize: number } }
| { event: "progress"; data: { downloaded: number; total: number } }
| { event: "completed"; data: { path: string } }
| { event: "error"; data: { message: string } };
async function downloadWithProgress(url: string) {
const channel = new Channel<DownloadEvent>();
channel.onmessage = (message) => {
switch (message.event) {
case "started":
console.log(`开始下载: ${message.data.url}`);
break;
case "progress":
const percent = (message.data.downloaded / message.data.total) * 100;
console.log(`下载进度: ${percent.toFixed(1)}%`);
updateProgressBar(percent);
break;
case "completed":
console.log(`下载完成: ${message.data.path}`);
break;
case "error":
console.error(`下载失败: ${message.data.message}`);
break;
}
};
await invoke("download_file", {
url,
savePath: "/path/to/save",
onEvent: channel,
});
}
实战示例
完整的用户认证流程
Rust 后端:
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter};
#[derive(Deserialize)]
struct LoginRequest {
username: String,
password: String,
}
#[derive(Serialize)]
struct User {
id: u64,
username: String,
token: String,
}
#[tauri::command]
async fn login(
app: AppHandle,
request: LoginRequest,
) -> Result<User, String> {
// 验证用户
if request.username == "admin" && request.password == "password" {
let user = User {
id: 1,
username: request.username,
token: "abc123".to_string(),
};
// 发送登录成功事件
app.emit("user-logged-in", &user).unwrap();
Ok(user)
} else {
Err("用户名或密码错误".to_string())
}
}
#[tauri::command]
fn logout(app: AppHandle) {
app.emit("user-logged-out", ()).unwrap();
}
前端代码:
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
interface User {
id: number;
username: string;
token: string;
}
// 监听登录状态变化
listen<User>("user-logged-in", (event) => {
localStorage.setItem("user", JSON.stringify(event.payload));
updateUIForLoggedInUser(event.payload);
});
listen("user-logged-out", () => {
localStorage.removeItem("user");
updateUIForGuest();
});
// 登录
async function handleLogin(username: string, password: string) {
try {
const user = await invoke<User>("login", {
request: { username, password }
});
console.log("登录成功:", user);
} catch (error) {
console.error("登录失败:", error);
showError(error as string);
}
}
// 登出
async function handleLogout() {
await invoke("logout");
}
最佳实践
1. 类型安全
为前后端定义共享的类型定义:
// types.ts
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
export interface User {
id: number;
name: string;
email: string;
}
// types.rs
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
pub struct ApiResponse<T> {
pub success: bool,
pub data: Option<T>,
pub error: Option<String>,
}
#[derive(Deserialize, Serialize)]
pub struct User {
pub id: u64,
pub name: String,
pub email: String,
}
2. 错误处理
统一错误格式,避免暴露敏感信息:
#[tauri::command]
fn safe_command() -> Result<String, String> {
match risky_operation() {
Ok(result) => Ok(result),
Err(e) => {
// 记录详细错误日志
log::error!("操作失败: {:?}", e);
// 返回用户友好的错误信息
Err("操作失败,请稍后重试".to_string())
}
}
}
3. 性能优化
- 使用异步 Command 处理 I/O 操作
- 对于大数据传输,使用 Channel 而非 Events
- 及时取消不再需要的事件监听
4. 安全考虑
- 所有 Command 都应该验证输入参数
- 敏感操作需要额外的权限检查
- 不要在 Events 中传输敏感数据