跳到主要内容

进程间通信

Tauri 的前后端通过 IPC(Inter-Process Communication,进程间通信)进行通信。Tauri 提供了两种主要的通信机制:Commands 和 Events。

通信机制对比

特性CommandEvent
调用方向前端 → 后端双向
返回值支持不支持
类型安全是(编译时检查)否(运行时解析)
适用场景请求-响应模式通知、广播、流数据
底层协议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 中传输敏感数据

下一步