Rust Tauri-Specta插件库的使用:实现类型安全的前端与Rust后端通信

Rust Tauri-Specta插件库的使用:实现类型安全的前端与Rust后端通信

Specta Logo

安装

cargo add specta
cargo add tauri-specta --features javascript,typescript

为自定义类型添加Specta支持

use specta::Type;
use serde::{Deserialize, Serialize};

// `specta::Type`宏让我们能够理解你的类型
// 我们已经为原始类型实现了`specta::Type`
// 如果你想使用外部crate中的类型,可能需要在Specta中启用相应的特性
#[derive(Serialize, Type)]
pub struct MyCustomReturnType {
    pub some_field: String,
}

#[derive(Deserialize, Type)]
pub struct MyCustomArgumentType {
    pub foo: String,
    pub bar: i32,
}

使用Specta注解Tauri命令

#[tauri::command]
#[specta::specta] // <-- 这是关键
fn greet3() -> MyCustomReturnType {
    MyCustomReturnType {
        some_field: "Hello World".into(),
    }
}

#[tauri::command]
#[specta::specta] // <-- 这是关键
fn greet(name: String) -> String {
  format!("Hello {name}!")
}

导出绑定

use specta::collect_types;
use tauri_specta::{ts, js};

// 这个示例在调试模式下或单元测试中启动时导出你的类型。你可以根据需要调整

fn main() {
    #[cfg(debug_assertions)]
    ts::export(collect_types![greet, greet2, greet3], "../src/bindings.ts").unwrap();

    // 或者导出带有JSDoc的JS
    #[cfg(debug_assertions)]
    js::export(collect_types![greet, greet2, greet3], "../src/bindings.js").unwrap();
}

#[test]
fn export_bindings() {
    ts::export(collect_types![greet, greet2, greet3], "../src/bindings.ts").unwrap();
    js::export(collect_types![greet, greet2, greet3], "../src/bindings.js").unwrap();
}

在前端使用

import * as commands from "./bindings"; // 应该指向我们从Rust导出的文件

await commands.greet("Brendan");

已知限制

  • 你的命令最多只能有10个参数。超过这个数量会导致编译错误。如果需要更多参数,可以使用结构体。
  • 在Tauri热重载跟踪的目录中导出你的模式会导致无限重载循环。

开发

运行示例:

pnpm i
cd example/
pnpm tauri dev

完整示例Demo

下面是一个完整的Tauri-Specta使用示例:

// main.rs
use specta::Type;
use serde::{Deserialize, Serialize};
use tauri_specta::{ts, js};
use specta::collect_types;

// 自定义返回类型
#[derive(Serialize, Type)]
pub struct User {
    pub id: i32,
    pub name: String,
    pub email: String,
}

// 自定义参数类型
#[derive(Deserialize, Type)]
pub struct CreateUserRequest {
    pub name: String,
    pub email: String,
}

// Tauri命令1 - 获取用户
#[tauri::command]
#[specta::specta]
fn get_user(id: i32) -> User {
    User {
        id,
        name: "John Doe".to_string(),
        email: "john@example.com".to_string(),
    }
}

// Tauri命令2 - 创建用户
#[tauri::command]
#[specta::specta]
fn create_user(user_data: CreateUserRequest) -> User {
    User {
        id: 1,
        name: user_data.name,
        email: user_data.email,
    }
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![get_user, create_user])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");

    // 在调试模式下导出TypeScript类型
    #[cfg(debug_assertions)]
    ts::export(
        collect_types![get_user, create_user, User, CreateUserRequest],
        "../src/bindings.ts"
    ).unwrap();
}

前端使用示例 (React + TypeScript):

// App.tsx
import React, { useState } from 'react';
import * as commands from './bindings'; // 导入生成的绑定

function App() {
  const [user, setUser] = useState<any>(null);
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const fetchUser = async () => {
    const user = await commands.getUser(1);
    setUser(user);
  };

  const createNewUser = async () => {
    const newUser = await commands.createUser({ name, email });
    setUser(newUser);
  };

  return (
    <div>
      <button onClick={fetchUser}>获取用户</button>
      
      <div>
        <h2>创建用户</h2>
        <input 
          value={name} 
          onChange={(e) => setName(e.target.value)} 
          placeholder="姓名"
        />
        <input
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="邮箱"
        />
        <button onClick={createNewUser}>创建</button>
      </div>

      {user && (
        <div>
          <h3>用户信息</h3>
          <p>ID: {user.id}</p>
          <p>姓名: {user.name}</p>
          <p>邮箱: {user.email}</p>
        </div>
      )}
    </div>
  );
}

export default App;

注意事项

  1. 确保在tauri.conf.json中正确配置了前端路径
  2. 开发时可以使用cargo tauri dev同时启动前端和后端
  3. 每次修改Rust命令后,需要重新运行应用以生成新的类型定义

1 回复

下面是基于您提供的内容整理的完整示例Demo,包含Rust后端和TypeScript前端的完整实现:

Rust后端实现 (main.rs)

use specta::{specta, Type};
use tauri::{command, Window};
use serde::Serialize;

// 1. 定义数据结构
#[derive(Type, Serialize)]
struct User {
    id: String,
    name: String,
    email: String,
}

#[derive(Type, Serialize)]
enum Status {
    Active,
    Inactive,
    Banned,
}

#[derive(Type, Serialize)]
struct ProgressEvent {
    progress: u8,
    message: String,
}

// 2. 定义Tauri命令
#[command]
#[specta]
fn get_user(id: String) -> Result<User, String> {
    if id.is_empty() {
        return Err("ID不能为空".to_string());
    }
    
    Ok(User {
        id,
        name: "张三".to_string(),
        email: "zhangsan@example.com".to_string(),
    })
}

#[command]
#[specta]
fn get_user_status(id: String) -> Status {
    match id.as_str() {
        "1" => Status::Active,
        "2" => Status::Inactive,
        _ => Status::Banned,
    }
}

#[command]
#[specta]
fn start_background_task(window: Window) {
    std::thread::spawn(move || {
        for i in 0..=100 {
            window
                .emit("task_progress", ProgressEvent {
                    progress: i,
                    message: format!("已完成 {}%", i),
                })
                .unwrap();
            std::thread::sleep(std::time::Duration::from_millis(50));
        }
    });
}

// 3. 配置Tauri-Specta
fn main() {
    let specta_builder = {
        let specta_builder = tauri_specta::ts::builder()
            .commands(tauri_specta::collect_types![
                get_user,
                get_user_status,
                start_background_task
            ]);
        
        #[cfg(debug_assertions)]
        let specta_builder = specta_builder.path("../src/bindings.ts");
        
        specta_builder
    };

    tauri::Builder::default()
        .invoke_handler(specta_builder.into_invoke_handler())
        .run(tauri::generate_context!())
        .expect("运行Tauri应用时出错");
}

前端TypeScript实现 (src/main.ts)

import { get_user, get_user_status, start_background_task } from './bindings';
import { listen } from '@tauri-apps/api/event';

// 1. 获取用户信息
async function fetchUser() {
    try {
        const result = await get_user("123");
        if (result.status === "ok") {
            console.log("用户数据:", result.data);
            document.getElementById('user-name')!.textContent = result.data.name;
            document.getElementById('user-email')!.textContent = result.data.email;
        } else {
            console.error("获取用户失败:", result.error);
        }
    } catch (error) {
        console.error("调用命令失败:", error);
    }
}

// 2. 获取用户状态
async function checkUserStatus() {
    const status = await get_user_status("1");
    console.log("用户状态:", status);
    document.getElementById('user-status')!.textContent = status;
}

// 3. 监听后台任务进度
listen<ProgressEvent>('task_progress', (event) => {
    console.log(event.payload.message);
    const progressBar = document.getElementById('progress-bar')!;
    progressBar.style.width = `${event.payload.progress}%`;
    progressBar.textContent = `${event.payload.progress}%`;
});

// 4. 启动后台任务
document.getElementById('start-task')!.addEventListener('click', () => {
    start_background_task();
});

// 初始化
fetchUser();
checkUserStatus();

前端HTML (src/index.html)

<!DOCTYPE html>
<html>
<head>
    <title>Tauri-Specta示例</title>
    <style>
        .progress-container {
            width: 100%;
            background-color: #f0f0f0;
            margin: 20px 0;
        }
        .progress-bar {
            height: 30px;
            background-color: #4CAF50;
            text-align: center;
            line-height: 30px;
            color: white;
            width: 0%;
        }
    </style>
</head>
<body>
    <h1>用户信息</h1>
    <div>
        <p>姓名: <span id="user-name"></span></p>
        <p>邮箱: <span id="user-email"></span></p>
        <p>状态: <span id="user-status"></span></p>
    </div>

    <button id="start-task">启动后台任务</button>
    
    <div class="progress-container">
        <div id="progress-bar" class="progress-bar">0%</div>
    </div>

    <script src="main.js" type="module"></script>
</body>
</html>

Cargo.toml 配置

[package]
name = "tauri-specta-demo"
version = "0.1.0"
edition = "2021"

[dependencies]
tauri = { version = "1", features = ["specta"] }
specta = { version = "1", features = ["export"] }
tauri-specta = "1"
serde = { version = "1", features = ["derive"] }

完整功能说明

  1. 用户数据获取

    • 后端定义get_user命令返回用户数据
    • 前端调用并显示用户信息
  2. 枚举类型使用

    • 后端定义Status枚举表示用户状态
    • 前端直接使用生成的类型安全枚举
  3. 事件系统

    • 后端通过start_background_task启动后台线程
    • 使用emit发送进度事件
    • 前端监听事件并更新进度条
  4. 错误处理

    • Rust命令返回Result类型
    • 前端处理可能的错误情况
  5. 类型安全

    • 所有类型在前后端自动同步
    • 前端调用时自动获得类型提示和检查
回到顶部