Chapter 04

Commands:前后端通信

用 #[tauri::command] 暴露 Rust 函数,invoke() 实现类型安全的 IPC 调用

Command 系统概述

Tauri 的 Commands 是前端与 Rust 后端通信的核心机制。工作流程如下:

前端(JavaScript/TypeScript) │ │ invoke('command_name', { arg1, arg2 }) │ ▼ Tauri IPC Bridge(消息序列化 JSON) │ ▼ Rust 后端 │ #[tauri::command] │ fn command_name(arg1: String, arg2: u32) -> Result<Data, Error> │ ▼ 返回值序列化为 JSON → 前端 Promise resolve

基础 Command 定义

// src-tauri/src/lib.rs
use tauri::AppHandle;

// 最简单的 Command:同步,接受参数,返回 String
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! From Rust.", name)
}

// 接受多个参数
#[tauri::command]
fn add_numbers(a: i64, b: i64) -> i64 {
    a + b
}

// 返回结构体(自动序列化为 JSON)
#[derive(serde::Serialize)]
struct SystemInfo {
    os: String,
    arch: String,
    cpu_count: usize,
}

#[tauri::command]
fn get_system_info() -> SystemInfo {
    SystemInfo {
        os: std::env::consts::OS.to_string(),
        arch: std::env::consts::ARCH.to_string(),
        cpu_count: num_cpus::get(),
    }
}

// 注册所有 Commands
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            greet,
            add_numbers,
            get_system_info,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

参数类型映射

TypeScript 类型Rust 类型说明
stringString / &str字符串
numberi32 / u64 / f64整数/浮点数
booleanbool布尔值
null / undefinedOption<T>可选值
T[]Vec<T>数组
Record<string, T>HashMap<String, T>键值对
object(带字段)#[derive(Deserialize)] struct自定义结构
Uint8ArrayVec<u8>二进制数据

接受结构体参数

use serde::Deserialize;

// 接受 JSON 对象,字段名用 snake_case(Tauri 自动处理驼峰转换)
#[derive(Deserialize)]
struct CreateFileArgs {
    path: String,
    content: String,
    overwrite: Option<bool>,  // 可选字段
}

#[tauri::command]
fn create_file(args: CreateFileArgs) -> Result<(), String> {
    let overwrite = args.overwrite.unwrap_or(false);
    if std::path::Path::new(&args.path).exists() && !overwrite {
        return Err("File already exists".to_string());
    }
    std::fs::write(&args.path, &args.content)
        .map_err(|e| e.to_string())
}
// 前端调用:字段名可用驼峰(Tauri 自动转为 snake_case)
await invoke('create_file', {
  path: '/tmp/test.txt',
  content: 'Hello, Tauri!',
  overwrite: true,
});

异步 Command

耗时操作(文件 IO、网络请求、计算密集型任务)应使用 async fn,避免阻塞主线程:

use tokio::time::{sleep, Duration};

// async Command 在 Tokio 运行时上执行,不阻塞 UI
#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
    let response = reqwest::get(&url)
        .await
        .map_err(|e| e.to_string())?;

    response.text().await.map_err(|e| e.to_string())
}

// 模拟耗时操作
#[tauri::command]
async fn process_large_file(path: String) -> Result<u64, String> {
    // 在 spawn_blocking 中运行 CPU 密集型任务,不占用 async 线程
    tokio::task::spawn_blocking(move || {
        let content = std::fs::read(&path)
            .map_err(|e| e.to_string())?;
        // 复杂计算...
        Ok(content.len() as u64)
    }).await
    .map_err(|e| e.to_string())?
}
// 前端:async Command 返回 Promise,可用 await 等待
try {
  const data = await invoke<string>('fetch_data', {
    url: 'https://api.example.com/data'
  });
  console.log(data);
} catch (error) {
  // Rust 返回 Err(...) 时,Promise reject,error 是错误字符串
  console.error('Failed:', error);
}

错误处理

use serde::Serialize;
use thiserror::Error;

// 自定义错误类型,实现 Serialize 才能传递到前端
#[derive(Debug, Error, Serialize)]
enum AppError {
    #[error("File not found: {path}")]
    FileNotFound { path: String },

    #[error("Permission denied")]
    PermissionDenied,

    #[error("IO error: {0}")]
    Io(
        #[from]
        #[serde(skip)]
        std::io::Error
    ),
}

#[tauri::command]
fn read_config(path: String) -> Result<String, AppError> {
    if !std::path::Path::new(&path).exists() {
        return Err(AppError::FileNotFound { path });
    }
    let content = std::fs::read_to_string(&path)?; // ? 触发 From 转换
    Ok(content)
}
// 前端接收结构化错误
try {
  const config = await invoke<string>('read_config', { path: 'config.toml' });
} catch (error) {
  // error 是 Rust 错误结构的 JSON 表示
  console.error(error); // "File not found: config.toml"
}

共享状态:State 管理

use std::sync::Mutex;
use tauri::State;

// 定义全局状态(用 Mutex 包装以实现线程安全)
struct AppState {
    counter: Mutex<i64>,
    config: Mutex<Option<String>>,
}

// 在 Command 中通过 State 访问共享状态
#[tauri::command]
fn increment(state: State<AppState>) -> i64 {
    let mut counter = state.counter.lock().unwrap();
    *counter += 1;
    *counter
}

#[tauri::command]
fn get_counter(state: State<AppState>) -> i64 {
    *state.counter.lock().unwrap()
}

// 注册时通过 manage() 注入初始状态
pub fn run() {
    tauri::Builder::default()
        .manage(AppState {
            counter: Mutex::new(0),
            config: Mutex::new(None),
        })
        .invoke_handler(tauri::generate_handler![increment, get_counter])
        .run(tauri::generate_context!())
        .expect("error");
}
Command 命名规范

Rust 函数名使用 snake_case(如 get_system_info),前端 invoke 时也使用 snake_case 字符串。Tauri 不做自动的驼峰/蛇形转换(仅参数字段名做转换)。保持一致的命名风格可减少调试时的困惑。

Command 参数的高级用法

Tauri Commands 支持一些特殊的参数注入,不需要从前端传递:

use tauri::{AppHandle, Window, State, Env};

// AppHandle:访问 Tauri 应用句柄(emit 事件、获取窗口等)
// Window:当前调用此 command 的窗口引用
// State:之前用 manage() 注入的状态
// 这些参数由 Tauri 自动注入,不需要前端传递
#[tauri::command]
async fn complex_operation(
    app: AppHandle,           // 自动注入:应用句柄
    window: Window,          // 自动注入:调用方窗口
    state: State<AppState>,  // 自动注入:共享状态
    user_arg: String,         // 从前端传来的参数
    count: u32,              // 从前端传来的参数
) -> Result<String, String> {
    // 获取当前窗口标题
    let title = window.title().unwrap_or_default();

    // 向前端发送进度事件
    app.emit("progress", 50).unwrap();

    // 访问共享状态
    let counter = state.counter.lock().unwrap();

    Ok(format!("Window: {}, Counter: {}, Arg: {}", title, *counter, user_arg))
}

Command 的多文件组织

随着项目增长,所有 Commands 放在一个文件会变得难以维护。推荐按功能模块拆分:

// src-tauri/src/commands/mod.rs
pub mod file_commands;
pub mod system_commands;
pub mod data_commands;

// src-tauri/src/commands/file_commands.rs
#[tauri::command]
pub fn read_file(path: String) -> Result<String, String> {
    std::fs::read_to_string(&path).map_err(|e| e.to_string())
}

#[tauri::command]
pub fn write_file(path: String, content: String) -> Result<(), String> {
    std::fs::write(&path, content).map_err(|e| e.to_string())
}

// src-tauri/src/commands/system_commands.rs
#[derive(serde::Serialize)]
pub struct SystemInfo {
    pub os: String,
    pub arch: String,
}

#[tauri::command]
pub fn get_system_info() -> SystemInfo {
    SystemInfo {
        os: std::env::consts::OS.to_string(),
        arch: std::env::consts::ARCH.to_string(),
    }
}

// src-tauri/src/lib.rs(汇总注册)
mod commands;
use commands::{file_commands::*, system_commands::*};

pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            read_file,
            write_file,
            get_system_info,
        ])
        .run(tauri::generate_context!())
        .unwrap();
}

完整实战:系统信息面板

综合运用同步/异步 Command、结构体序列化和错误处理,实现一个系统信息查询面板:

// src-tauri/src/lib.rs
use serde::Serialize;

#[derive(Serialize)]
pub struct DetailedSystemInfo {
    os_name: String,
    os_version: String,
    architecture: String,
    cpu_count: usize,
    hostname: String,
}

#[tauri::command]
pub fn get_detailed_info() -> Result<DetailedSystemInfo, String> {
    let hostname = hostname::get()
        .map(|h| h.to_string_lossy().to_string())
        .unwrap_or_else(|_| "unknown".to_string());

    Ok(DetailedSystemInfo {
        os_name: std::env::consts::OS.to_string(),
        os_version: os_info::get().version().to_string(),
        architecture: std::env::consts::ARCH.to_string(),
        cpu_count: num_cpus::get(),
        hostname,
    })
}
import { invoke } from '@tauri-apps/api/core';
import { useEffect, useState } from 'react';

interface DetailedSystemInfo {
  osName: string;
  osVersion: string;
  architecture: string;
  cpuCount: number;
  hostname: string;
}

function SystemInfoPanel() {
  const [info, setInfo] = useState<DetailedSystemInfo | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    invoke<DetailedSystemInfo>('get_detailed_info')
      .then(setInfo)
      .catch(e => setError(String(e)));
  }, []);

  if (error) return <p className="error">Error: {error}</p>;
  if (!info) return <p>Loading...</p>;

  return (
    <table>
      <tr><td>操作系统</td><td>{info.osName} {info.osVersion}</td></tr>
      <tr><td>架构</td><td>{info.architecture}</td></tr>
      <tr><td>CPU 核心数</td><td>{info.cpuCount}</td></tr>
      <tr><td>主机名</td><td>{info.hostname}</td></tr>
    </table>
  );
}

invoke 调用的常见错误

command not found
Command 未在 generate_handler! 中注册,或函数名拼写错误(大小写敏感,必须完全匹配)。
invalid args
前端传递的参数类型与 Rust 参数类型不匹配,如传 number 但 Rust 期望 String,或缺少必填参数。
Permission denied
没有在 capabilities 中声明对应插件的权限,Tauri 的安全层拦截了调用。
不能在 SSR 中调用 invoke
使用 SvelteKit/Next.js 等 SSR 框架时,invoke 只能在浏览器(客户端)环境调用,不能在服务端渲染阶段调用。确保在 onMount/useEffect 等客户端生命周期中使用。

本章小结

本章核心要点