mcpField
C
calling-rust-from-tauri-frontend

calling-rust-from-tauri-frontend

github.com/dchuk开发36350 次安装40 周前更新
代码仓库

关于 calling-rust-from-tauri-frontend

面向 Tauri v2 开发者的技能,介绍如何从 JavaScript 或 TypeScript 前端调用 Rust 命令,并处理参数、返回值、错误、异步调用和数据流。

主要功能

  1. 01注册并调用命令
  2. 02映射 Rust 与 JavaScript 参数
  3. 03处理序列化结果和错误
  4. 04异步命令与流式通道

适用场景

  1. 01让 Tauri 前端调用 Rust 函数
  2. 02返回结构化数据或二进制响应
  3. 03在前端处理后端错误

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dchuk/claude-code-tauri-skills --skill calling-rust-from-tauri-frontend

安装后在 Claude Code 中输入/calling-rust-from-tauri-frontend确认已加载。

详细介绍

从 Tauri 前端调用 Rust

本技能介绍如何在 Tauri v2 前端中使用命令系统和 invoke 函数调用 Rust 后端函数。

概述

Tauri 提供两种 IPC 机制:

  • 命令(推荐):带有序列化参数和返回值的类型安全函数调用
  • 事件:动态的单向通信(此处不作介绍)

基本命令

在 Rust 中定义命令

使用 #[tauri::command] 属性宏:

// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: String) -> String {
    format!("Hello, {}!", name)
}
注册命令

必须通过 invoke 处理程序注册命令:

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet, login, fetch_data])
        .run(tauri::generate_context!())
        .expect("error while running tauri application")
}
从 JavaScript/TypeScript 调用
import { invoke } from '@tauri-apps/api/core';

const greeting = await invoke('greet', { name: 'World' });
console.log(greeting); // "Hello, World!"

或者使用全局 Tauri 对象(启用 app.withGlobalTauri 时):

const { invoke } = window.__TAURI__.core;
const greeting = await invoke('greet', { name: 'World' });

传递参数

参数命名约定

默认情况下,Rust 的 snake_case 参数会映射为 JavaScript 的 camelCase:

#[tauri::command]
fn create_user(user_name: String, user_age: u32) -> String {
    format!("{} is {} years old", user_name, user_age)
}
await invoke('create_user', { userName: 'Alice', userAge: 30 });

使用 rename_all 更改命名约定:

#[tauri::command(rename_all = "snake_case")]
fn create_user(user_name: String, user_age: u32) -> String {
    format!("{} is {} years old", user_name, user_age)
}
复杂参数

参数必须实现 serde::Deserialize:

use serde::Deserialize;

#[derive(Deserialize)]
struct UserData {
    name: String,
    email: String,
    age: u32,
}

#[tauri::command]
fn register_user(user: UserData) -> String {
    format!("Registered {} ({}) age {}", user.name, user.email, user.age)
}
await invoke('register_user', {
    user: { name: 'Alice', email: '[email protected]', age: 30 }
});

返回值

简单返回类型

返回类型必须实现 serde::Serialize:

#[tauri::command]
fn get_count() -> i32 { 42 }

#[tauri::command]
fn get_message() -> String { "Hello from Rust!".into() }
const count: number = await invoke('get_count');
const message: string = await invoke('get_message');
返回复杂类型
use serde::Serialize;

#[derive(Serialize)]
struct AppConfig {
    theme: String,
    language: String,
    notifications_enabled: bool,
}

#[tauri::command]
fn get_config() -> AppConfig {
    AppConfig {
        theme: "dark".into(),
        language: "en".into(),
        notifications_enabled: true,
    }
}
interface AppConfig {
    theme: string;
    language: string;
    notificationsEnabled: boolean;
}
const config: AppConfig = await invoke('get_config');
返回二进制数据

对于大型二进制数据,使用 tauri::ipc::Response 绕过 JSON 序列化:

use tauri::ipc::Response;

#[tauri::command]
fn read_file(path: String) -> Response {
    let data = std::fs::read(&path).unwrap();
    Response::new(data)
}
const data: ArrayBuffer = await invoke('read_file', { path: '/path/to/file' });

错误处理

使用 Result 类型

返回 Result<T, E>,其中 E 实现 Serialize 或为 String:

#[tauri::command]
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Cannot divide by zero".into())
    } else {
        Ok(a / b)
    }
}
try {
    const result = await invoke('divide', { a: 10, b: 0 });
} catch (error) {
    console.error('Error:', error); // "Cannot divide by zero"
}
使用 thiserror 自定义错误类型
use serde::Serialize;
use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("File not found: {0}")]
    FileNotFound(String),
    #[error("Permission denied")]
    PermissionDenied,
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

impl Serialize for AppError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where S: serde::ser::Serializer {
        serializer.serialize_str(self.to_string().as_ref())
    }
}

#[tauri::command]
fn open_file(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)?;
    Ok(content)
}
结构化错误响应
use serde::Serialize;

#[derive(Debug, Serialize)]
struct ErrorResponse { code: String, message: String }

#[tauri::command]
fn validate_input(input: String) -> Result<String, ErrorResponse> {
    if input.is_empty() {
        return Err(ErrorResponse {
            code: "EMPTY_INPUT".into(),
            message: "Input cannot be empty".into(),
        });
    }
    Ok(input.to_uppercase())
}
interface ErrorResponse { code: string; message: string; }

try {
    const result = await invoke('validate_input', { input: '' });
} catch (error) {
    const err = error as ErrorResponse;
    console.error(`Error ${err.code}: ${err.message}`);
}

异步命令

定义异步命令

使用 async 关键字:

#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
    let response = reqwest::get(&url).await.map_err(|e| e.to_string())?;
    let body = response.text().await.map_err(|e| e.to_string())?;
    Ok(body)
}
异步命令使用借用类型的限制

异步命令不能直接使用 &str 之类的借用类型:

// Will NOT compile:
// async fn bad_command(value: &str) -> String { ... }

// Use owned types instead:
#[tauri::command]
async fn good_command(value: String) -> String {
    some_async_operation(&value).await;
    value
}

// Or wrap in Result as workaround:
#[tauri::command]
async fn with_borrowed(value: &str) -> Result<String, ()> {
    some_async_operation(value).await;
    Ok(value.to_string())
}
前端调用

异步命令与同步命令的调用方式相同,因为 invoke 会返回一个 Promise:

const result = await invoke('fetch_data', { url: 'https://api.example.com/data' });

访问 Tauri 内部对象

WebviewWindow、AppHandle 和 State
use std::sync::Mutex;

struct AppState { counter: Mutex<i32> }

#[tauri::command]
async fn get_window_label(window: tauri::WebviewWindow) -> String {
    window.label().to_string()
}

#[tauri::command]
async fn get_app_version(app: tauri::AppHandle) -> String {
    app.package_info().version.to_string()
}

#[tauri::command]
fn increment_counter(state: tauri::State<AppState>) -> i32 {
    let mut counter = state.counter.lock().unwrap();
    *counter += 1;
    *counter
}

pub fn run() {
    tauri::Builder::default()
        .manage(AppState { counter: Mutex::new(0) })
        .invoke_handler(tauri::generate_handler![
            get_window_label, get_app_version, increment_counter
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application")
}

高级功能

访问原始请求

访问标头和原始请求正文:

use tauri::ipc::{Request, InvokeBody};

#[tauri::command]
fn upload(request: Request) -> Result<String, String> {
    let InvokeBody::Raw(data) = request.body() else {
        return Err("Expected raw body".into());
    };
    let auth = request.headers()
        .get("Authorization")
        .and_then(|v| v.to_str().ok())
        .ok_or("Missing Authorization header")?;
    Ok(format!("Received {} bytes", data.len()))
}
const data = new Uint8Array([1, 2, 3, 4, 5]);
await invoke('upload', data, { headers: { Authorization: 'Bearer token123' } });
使用 Channel 进行流式传输
use tauri::ipc::Channel;
use tokio::io::AsyncReadExt;

#[tauri::command]
async fn stream_file(path: String, channel: Channel<Vec<u8>>) -> Result<(), String> {
    let mut file = tokio::fs::File::open(&path).await.map_err(|e| e.to_string())?;
    let mut buffer = vec![0u8; 4096];
    loop {
        let len = file.read(&mut buffer).await.map_err(|e| e.to_string())?;
        if len == 0 { break; }
        channel.send(buffer[..len].to_vec()).map_err(|e| e.to_string())?;
    }
    Ok(())
}
import { Channel } from '@tauri-apps/api/core';

const channel = new Channel<Uint8Array>();
channel.onmessage = (chunk) => console.log('Received:', chunk.length, 'bytes');
await invoke('stream_file', { path: '/path/to/file', channel });

在模块中组织命令

// src-tauri/src/commands/user.rs
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
pub struct CreateUserRequest { pub name: String, pub email: String }

#[derive(Serialize)]
pub struct User { pub id: u32, pub name: String, pub email: String }

#[tauri::command]
pub fn create_user(request: CreateUserRequest) -> User {
    User { id: 1, name: request.name, email: request.email }
}
// src-tauri/src/commands/mod.rs
pub mod user;
// src-tauri/src/lib.rs
mod commands;

pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![commands::user::create_user])
        .run(tauri::generate_context!())
        .expect("error while running tauri application")
}

TypeScript 类型安全

创建一个带类型的包装器:

import { invoke } from '@tauri-apps/api/core';

export interface User { id: number; name: string; email: string; }
export interface CreateUserRequest { name: string; email: string; }

export const commands = {
    createUser: (request: CreateUserRequest): Promise<User> =>
        invoke('create_user', { request }),
    greet: (name: string): Promise<string> =>
        invoke('greet', { name }),
};

// Usage
const user = await commands.createUser({ name: 'Bob', email: '[email protected]' });

快速参考

任务RustJavaScript
定义命令#[tauri::command] fn name() {}-
注册命令tauri::generate_handler![name]-
调用命令-await invoke('name', { args })
返回值-> T,其中 T: Serializeconst result = await invoke(...)
返回错误-> Result<T, E>try/catch
异步命令async fn name()与同步命令相同
访问窗口window: tauri::WebviewWindow-
访问应用app: tauri::AppHandle-
访问状态state: tauri::State<T>-

主要限制

  1. 整个应用中的命令名称必须唯一
  2. lib.rs 中的命令不能是 pub(可使用模块进行组织)
  3. 所有命令都必须在同一个 generate_handler! 调用中注册
  4. 异步命令不能直接使用 &str 之类的借用类型
  5. 参数必须实现 Deserialize,返回类型必须实现 Serialize

评论

支持 Markdown