跳到正文
灵动桌面框架
Esc
本栏目菜单

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)
  • 大数据传输考虑分批次或使用事件流