Rust Tauri-Specta插件库的使用:实现类型安全的前端与Rust后端通信
Rust Tauri-Specta插件库的使用:实现类型安全的前端与Rust后端通信
安装
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;
注意事项
- 确保在
tauri.conf.json
中正确配置了前端路径 - 开发时可以使用
cargo tauri dev
同时启动前端和后端 - 每次修改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"] }
完整功能说明
-
用户数据获取:
- 后端定义
get_user
命令返回用户数据 - 前端调用并显示用户信息
- 后端定义
-
枚举类型使用:
- 后端定义
Status
枚举表示用户状态 - 前端直接使用生成的类型安全枚举
- 后端定义
-
事件系统:
- 后端通过
start_background_task
启动后台线程 - 使用
emit
发送进度事件 - 前端监听事件并更新进度条
- 后端通过
-
错误处理:
- Rust命令返回
Result
类型 - 前端处理可能的错误情况
- Rust命令返回
-
类型安全:
- 所有类型在前后端自动同步
- 前端调用时自动获得类型提示和检查