IPC 通信
IPC(进程间通信)是连接前端 UI 和 Rust 后端的桥梁。灵动桌面框架提供了封装良好的 API 层来简化 IPC 调用。
invoke 调用流程
1. 定义 Rust Command
// src-tauri/src/commands/system.rs
use crate::error::CommandError;
/// 简单的 greet 命令(保留为示例)
#[tauri::command]
pub fn greet(name: &str) -> Result<String, CommandError> {
if name.is_empty() {
return Err(CommandError {
code: "INVALID_INPUT".to_string(),
message: "名称不能为空".to_string(),
});
}
Ok(format!("Hello, {}! 来自 Rust 的问候!", name))
}
带业务逻辑的 Command 只做参数接收,再交给 Service 层,并把内部错误 AppError 转成 CommandError:
// src-tauri/src/commands/config.rs
#[tauri::command]
pub fn get_config(state: tauri::State<'_, AppState>, key: String) -> Result<String, CommandError> {
ConfigService::get(&state.db, &key).map_err(|e| e.into())
}
2. 注册 Command
// src-tauri/src/lib.rs
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
// 系统模块
commands::system::greet,
commands::system::get_system_info,
// 配置模块
commands::config::get_all_config,
commands::config::get_config,
commands::config::set_config,
commands::config::delete_config,
])
3. 前端 API 封装
每个业务模块一个文件,导出一个 xxxApi 对象:
// src/lib/api/system.ts
import { invoke } from "./client";
import type { SystemInfo } from "@/types";
/** 系统相关 API */
export const systemApi = {
greet: (name: string) => invoke<string>("greet", { name }),
getSystemInfo: () => invoke<SystemInfo>("get_system_info"),
};
再在 src/lib/api/index.ts 统一导出(目前有 systemApi、configApi、updaterApi)。
4. 在组件中使用
import { systemApi, getErrorMessage } from '@/lib/api'
function MyComponent() {
const handleClick = async () => {
try {
const message = await systemApi.greet('灵动')
console.log(message) // "Hello, 灵动! 来自 Rust 的问候!"
} catch (e) {
console.error(getErrorMessage(e))
}
}
return <button onClick={handleClick}>打招呼</button>
}
API Client 封装
client.ts 直接再导出 Tauri 的 invoke,供各 API 模块统一引用,并提供后端结构化错误 CommandError({ code, message })的解析工具:
// src/lib/api/client.ts
import { invoke } from "@tauri-apps/api/core";
/** 后端结构化错误响应 */
export interface CommandError {
code: string;
message: string;
}
/**
* 解析后端 CommandError 结构化错误
* 如果是 JSON 格式则解析为 CommandError,否则返回纯文本错误
*/
export function parseCommandError(error: unknown): CommandError {
const raw = typeof error === "string" ? error : String(error);
try {
const parsed = JSON.parse(raw);
if (parsed.code && parsed.message) {
return parsed as CommandError;
}
} catch {
// 非 JSON 格式,降级为通用错误
}
return { code: "UNKNOWN", message: raw };
}
/** 获取错误消息(用于 UI 展示) */
export function getErrorMessage(error: unknown): string {
return parseCommandError(error).message;
}
/** 获取错误码(用于程序判断) */
export function getErrorCode(error: unknown): string {
return parseCommandError(error).code;
}
export { invoke };
后端错误码(CommandError.code)取值:IO_ERROR、DATABASE_ERROR、JSON_ERROR、NOT_FOUND、INVALID_INPUT、INTERNAL,由 src-tauri/src/error.rs 中 AppError → CommandError 的转换决定;前端解析失败时降级为 UNKNOWN。
useCommand Hook
适合「进入页面就拉一次数据」这类场景,自带 loading / error 状态。参数在调用 Hook 时传入,execute() 本身不带参数,失败时不抛异常而是返回 null 并写入 error:
// src/hooks/useCommand.ts
import { useState, useCallback, useRef } from "react";
import { invoke } from "@tauri-apps/api/core";
interface UseCommandResult<T> {
data: T | null;
error: string | null;
loading: boolean;
execute: (...args: unknown[]) => Promise<T | null>;
}
export function useCommand<T>(
command: string,
args?: Record<string, unknown>
): UseCommandResult<T> {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const argsRef = useRef(args);
argsRef.current = args;
const execute = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await invoke<T>(command, argsRef.current);
setData(result);
return result;
} catch (e) {
const msg = typeof e === "string" ? e : String(e);
setError(msg);
return null;
} finally {
setLoading(false);
}
}, [command]);
return { data, error, loading, execute };
}
同一文件还导出 safeInvoke<T>(command, args?):失败时打印 [Command] xxx 失败 日志并抛出 Error,适合不需要 Hook 状态的一次性调用。
事件通信
除了请求-响应式的 invoke,还支持事件驱动的通信(模板目前没有内置自定义事件,下面的事件名仅为示例):
// Rust 端发送事件
use tauri::Emitter;
app_handle.emit("task-progress", 50)?;
// 前端监听
import { listen } from '@tauri-apps/api/event'
const unlisten = await listen<number>('task-progress', (event) => {
console.log('进度:', event.payload)
})
// 清理监听(组件卸载时调用)
unlisten()
应用更新不走自定义事件
检查更新使用 updater 插件的前端 API:updaterApi.checkUpdate()(即 @tauri-apps/plugin-updater 的 check()),设置页拿到结果后用 UpdateModal 展示。
最佳实践
规范
- 所有 invoke 调用必须封装到
src/lib/api/中,禁止在组件中直接使用invoke() - 每个 Command 必须返回
Result<T, CommandError>,禁止使用unwrap() - 新增 Command 后必须加进
lib.rs的generate_handler![],否则前端调用会报找不到命令 - 前端
invoke的命令名与 Rust 函数名一致,使用 snake_case(如get_system_info) - 大数据传输考虑分批次或使用事件流