Files
tauri-shadcn-vite-project/docs/开发指南.md
shenjianZ b2754bdad5 feat: 添加二维码生成功能并简化架构
- 新增二维码生成服务层,支持自定义内容和配置
  - 移除 Platform 抽象层,简化为三层架构
  - 更新开发文档和架构说明
  - 添加前端二维码生成页面和状态管理
2026-02-10 19:06:36 +08:00

23 KiB
Raw Blame History

Tauri 应用开发指南

目录


架构概述

三层架构

┌─────────────────────────────────────────────────┐
│                   Command 层                     │
│  职责:参数验证、调用 Service、结果转换          │
│  文件src/commands/*.rs                         │
├─────────────────────────────────────────────────┤
│                   Service 层                     │
│  职责:业务逻辑、流程编排、状态管理              │
│  文件src/services/*.rs                         │
├─────────────────────────────────────────────────┤
│                   Utils 层                       │
│  职责:纯算法、无状态工具函数                    │
│  文件src/utils/*.rs                            │
└─────────────────────────────────────────────────┘

依赖原则

  • 单向依赖:上层依赖下层,下层不依赖上层
  • 依赖链Command → Service → Utils
  • Utils 完全独立:只依赖标准库和 models
  • Service 可以直接调用 Windows API

添加新功能的完整流程

步骤 1需求分析

在开始编码前,明确以下内容:

  1. 功能描述:这个功能做什么?
  2. 用户交互:用户如何触发这个功能?
  3. 输入输出:需要什么参数?返回什么结果?
  4. 错误场景:哪些情况下会出错?如何处理?

步骤 2设计数据模型如需要

如果新功能需要新的数据结构,在 models/ 中定义:

// src/models/new_feature.rs

use serde::{Deserialize, Serialize};

/// 新功能的配置信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewFeatureConfig {
    /// 配置项 1
    pub option1: String,
    /// 配置项 2
    pub option2: u32,
}

/// 新功能的输出结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewFeatureResult {
    /// 状态码
    pub status: u16,
    /// 结果数据
    pub data: String,
}

规范

  • 使用中文注释
  • 每个字段都要有注释说明含义
  • 实现 Debug, Clone, Serialize, Deserialize
  • 结构体命名使用大驼峰PascalCase
  • 字段命名使用下划线命名snake_case

步骤 3实现工具函数如果需要纯算法

如果有纯算法逻辑,在 utils/ 中实现:

// src/utils/new_algorithm.rs

/// 新算法函数
///
/// 对输入数据进行处理
///
/// # 参数
///
/// * `input` - 输入数据
///
/// # 返回
///
/// 返回处理后的结果
///
/// # 示例
///
/// ```
/// use crate::utils::new_algorithm::process_data;
///
/// let result = process_data(42);
/// assert_eq!(result, 84);
/// ```
pub fn process_data(input: u32) -> u32 {
    input * 2
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_process_data() {
        assert_eq!(process_data(2), 4);
        assert_eq!(process_data(0), 0);
    }
}

规范

  • 纯函数,无副作用
  • 添加详细的文档注释
  • 包含单元测试
  • 不依赖任何外部状态

步骤 4实现业务逻辑Service 层)

// src/services/new_feature_service.rs

//! 新功能服务
//!
//! 提供新功能的核心业务逻辑

use crate::error::AppResult;
use crate::models::new_feature::{NewFeatureConfig, NewFeatureResult};

/// 新功能服务
pub struct NewFeatureService;

impl NewFeatureService {
    /// 执行新功能
    ///
    /// 根据配置执行相应的操作
    ///
    /// # 参数
    ///
    /// * `config` - 功能配置
    ///
    /// # 返回
    ///
    /// 返回执行结果
    ///
    /// # 错误
    ///
    /// - 配置无效时返回 `AppError::InvalidColorData`
    pub fn execute(&self, config: &NewFeatureConfig) -> AppResult<NewFeatureResult> {
        // 1. 参数验证
        if config.option1.is_empty() {
            return Err(AppError::InvalidColorData(
                "配置项 option1 不能为空".to_string()
            ));
        }

        // 2. 业务逻辑
        let result = format!("处理: {} - {}", config.option1, config.option2);

        // 3. 返回结果
        Ok(NewFeatureResult {
            status: 200,
            data: result,
        })
    }

    /// 执行新功能(异步版本)
    ///
    /// 异步执行新功能
    pub async fn execute_async(&self, config: NewFeatureConfig) -> AppResult<NewFeatureResult> {
        // 使用 tokio 或其他异步运行时
        Ok(self.execute(&config)?)
    }
}

规范

  • 使用 struct 命名空间(如 NewFeatureService
  • 所有方法返回 AppResult<T>
  • 参数验证放在 Service 层
  • 可以直接调用 Windows API
  • 添加详细的文档注释
  • 包含同步和异步两个版本(如需要)

步骤 5创建 Tauri 命令Command 层)

// src/commands/new_feature_commands.rs

//! 新功能命令
//!
//! 定义新功能相关的 Tauri 命令

use crate::models::new_feature::{NewFeatureConfig, NewFeatureResult};
use crate::services::new_feature_service::NewFeatureService;

/// 执行新功能命令
///
/// Tauri 命令,用于从前端调用新功能
///
/// # 参数
///
/// * `config` - 功能配置
///
/// # 返回
///
/// 返回执行结果,包含状态码和数据
///
/// # 前端调用示例
///
/// ```typescript
/// import { invoke } from '@tauri-apps/api/tauri';
///
/// const result = await invoke('execute_new_feature', {
///   config: {
///     option1: 'test',
///     option2: 100
///   }
/// });
/// console.log(result.status); // 200
/// console.log(result.data);   // "处理: test - 100"
/// ```
#[tauri::command]
pub fn execute_new_feature(config: NewFeatureConfig) -> Result<NewFeatureResult, String> {
    // 创建服务实例
    let service = NewFeatureService;

    // 调用服务层
    service
        .execute(&config)
        .map_err(|e| e.to_string())
}

/// 异步执行新功能命令
///
/// 异步版本的执行命令
///
/// # 参数
///
/// * `config` - 功能配置
///
/// # 返回
///
/// 返回执行结果
#[tauri::command]
pub async fn execute_new_feature_async(config: NewFeatureConfig) -> Result<NewFeatureResult, String> {
    let service = NewFeatureService;
    service
        .execute_async(config)
        .await
        .map_err(|e| e.to_string())
}

规范

  • 使用 #[tauri::command]
  • 返回类型为 Result<T, String>
  • 使用 .map_err(|e| e.to_string()) 转换错误
  • 包含前端调用示例
  • 参数使用结构体,便于扩展
  • 添加详细的文档注释

步骤 6注册模块

更新各模块的 mod.rslib.rs

// src/models/mod.rs
pub mod color;
pub mod new_feature; // 新增

pub use color::{ColorInfo, RgbInfo, HslInfo};
pub use new_feature::{NewFeatureConfig, NewFeatureResult}; // 新增

// src/commands/mod.rs
pub mod color_commands;
pub mod window_commands;
pub mod new_feature_commands; // 新增

// src/lib.rs
.invoke_handler(tauri::generate_handler![
    // ... 其他命令
    commands::new_feature_commands::execute_new_feature,
    commands::new_feature_commands::execute_new_feature_async,
])

规范

  • 按字母顺序排列
  • 导出常用类型
  • 注册所有新命令

步骤 7错误处理扩展如需要

如果需要新的错误类型,在 error.rs 中添加:

// src/error.rs

#[derive(Debug)]
pub enum AppError {
    // ... 现有错误类型

    /// 新功能相关错误
    NewFeatureFailed(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            // ... 现有错误
            AppError::NewFeatureFailed(msg) => write!(f, "新功能执行失败: {}", msg),
        }
    }
}

规范

  • 错误类型使用中文描述
  • 携带详细的错误信息(String
  • Display 实现中添加上下文

步骤 8更新前端类型定义

src/types/ 或相关位置添加 TypeScript 类型定义:

// src/types/new-feature.ts

/**
 * 新功能配置
 */
export interface NewFeatureConfig {
  /** 配置项 1 */
  option1: string;
  /** 配置项 2 */
  option2: number;
}

/**
 * 新功能结果
 */
export interface NewFeatureResult {
  /** 状态码 */
  status: number;
  /** 结果数据 */
  data: string;
}

// 导出 Tauri 命令类型
export type NewFeatureCommands = {
  execute_new_feature: (config: NewFeatureConfig) => Promise<NewFeatureResult>;
  execute_new_feature_async: (config: NewFeatureConfig) => Promise<NewFeatureResult>;
};

步骤 9编写测试

// src/services/new_feature_service.rs 中的测试

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_execute_with_valid_config() {
        let service = NewFeatureService;
        let config = NewFeatureConfig {
            option1: "test".to_string(),
            option2: 100,
        };

        let result = service.execute(&config).unwrap();
        assert_eq!(result.status, 200);
    }

    #[test]
    fn test_execute_with_empty_option1() {
        let service = NewFeatureService;
        let config = NewFeatureConfig {
            option1: "".to_string(),
            option2: 100,
        };

        assert!(service.execute(&config).is_err());
    }
}

规范

  • 测试函数命名使用 test_ 前缀
  • 测试正常场景和错误场景
  • 使用 assert!assert_eq! 断言
  • 运行 cargo test 验证

步骤 10验证和测试

# 1. 检查代码编译
cargo check

# 2. 运行测试
cargo test

# 3. 构建应用
pnpm build

# 4. 运行应用
pnpm tauri dev

# 5. 测试功能
# - 在前端界面测试新功能
# - 检查控制台输出
# - 验证错误处理

各层开发规范

Models 层规范

职责:定义数据结构,不包含业务逻辑

规范清单

  • 使用 serdeSerializeDeserialize
  • 实现 DebugClone
  • 所有字段必须有文档注释
  • 使用 new() 方法构造复杂对象
  • 字段类型优先使用基本类型
  • 集中定义在一个模块中

示例

/// 用户信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInfo {
    /// 用户 ID
    pub id: u32,
    /// 用户名
    pub name: String,
}

impl UserInfo {
    /// 创建新用户
    pub fn new(id: u32, name: String) -> Self {
        Self { id, name }
    }
}

Utils 层规范

职责:提供纯函数算法,无副作用

规范清单

  • 纯函数,不依赖外部状态
  • 不使用 unsafe 代码
  • 不执行 I/O 操作
  • 包含单元测试
  • 使用清晰的函数命名

示例

/// 计算两点之间的距离
pub fn calculate_distance(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
    let dx = x2 - x1;
    let dy = y2 - y1;
    (dx * dx + dy * dy).sqrt()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_calculate_distance() {
        let dist = calculate_distance(0.0, 0.0, 3.0, 4.0);
        assert_eq!(dist, 5.0);
    }
}

Service 层规范

职责:业务逻辑实现和流程编排

规范清单

  • 使用 struct 命名空间
  • 所有方法返回 AppResult<T>
  • 参数验证在 Service 层进行
  • 可以直接调用 Windows API
  • 可以调用 Utils 层函数
  • 提供同步和异步版本(如需要)
  • 使用详细的中文档注释

示例

pub struct FileService;

impl FileService {
    pub fn read_and_process(&self, path: &str) -> AppResult<String> {
        // 1. 参数验证
        if path.is_empty() {
            return Err(AppError::InvalidData("路径不能为空".to_string()));
        }

        // 2. 调用 Windows API 或第三方库
        let content = std::fs::read_to_string(path)?;

        // 3. 调用 Utils 层处理
        let processed = utils::text::trim_whitespace(&content);

        Ok(processed.to_string())
    }
}

Command 层规范

职责Tauri 命令处理,连接前端和 Service

规范清单

  • 使用 #[tauri::command]
  • 返回 Result<T, String>
  • 参数使用结构体(便于扩展)
  • 简洁的适配器代码
  • 包含前端调用示例
  • 错误转换为字符串

示例

#[tauri::command]
pub fn process_file(path: String) -> Result<String, String> {
    let service = FileService;
    service
        .read_and_process(&path)
        .map_err(|e| e.to_string())
}

代码示例

完整示例:添加一个屏幕截图功能

1. 定义数据模型

// src/models/screenshot.rs

use serde::{Deserialize, Serialize};

/// 截图配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScreenshotConfig {
    /// 是否包含光标
    pub include_cursor: bool,
    /// JPEG 质量 (1-100)
    pub quality: u8,
}

/// 截图结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScreenshotResult {
    /// 图片数据Base64
    pub data: String,
    /// 图片格式
    pub format: String,
    /// 图片宽度
    pub width: u32,
    /// 图片高度
    pub height: u32,
}

2. 实现 Service

// src/services/screenshot_service.rs

//! 屏幕截图服务
//!
//! 提供屏幕截图功能

use crate::error::{AppError, AppResult};
use crate::models::screenshot::{ScreenshotConfig, ScreenshotResult};

/// 截图服务
pub struct ScreenshotService;

impl ScreenshotService {
    /// 截取屏幕
    ///
    /// 根据配置截取屏幕画面
    ///
    /// # 参数
    ///
    /// * `config` - 截图配置
    ///
    /// # 返回
    ///
    /// 返回截图结果
    pub fn capture(config: &ScreenshotConfig) -> AppResult<ScreenshotResult> {
        // 验证配置
        if config.quality < 1 || config.quality > 100 {
            return Err(AppError::InvalidColorData(
                "图片质量必须在 1-100 之间".to_string()
            ));
        }

        // 调用 Windows API 或使用第三方库
        // 例如:使用 screenshots-rs crate
        let screenshots = screenshots::Screen::all().map_err(|e| {
            AppError::InvalidData(format!("获取屏幕失败: {}", e))
        })?;

        if let Some(screen) = screenshots.first() {
            let image = screen.capture().map_err(|e| {
                AppError::InvalidData(format!("截图失败: {}", e))
            })?;

            // 转换为 Base64
            let buffer = image.buffer();
            let base64_data = base64::encode(buffer);

            Ok(ScreenshotResult {
                data: base64_data,
                format: "png".to_string(),
                width: image.width(),
                height: image.height(),
            })
        } else {
            Err(AppError::InvalidData("未找到屏幕".to_string()))
        }
    }

    /// 截取指定区域
    pub fn capture_region(
        x: u32,
        y: u32,
        width: u32,
        height: u32,
        config: &ScreenshotConfig,
    ) -> AppResult<ScreenshotResult> {
        if width == 0 || height == 0 {
            return Err(AppError::InvalidColorData(
                "宽度和高度必须大于 0".to_string()
            ));
        }

        // 调用 Windows API 实现区域截图
        // ...
        Ok(ScreenshotResult {
            data: "base64_data".to_string(),
            format: "png".to_string(),
            width,
            height,
        })
    }
}

3. 创建命令

// src/commands/screenshot_commands.rs

//! 截图命令

use crate::models::screenshot::{ScreenshotConfig, ScreenshotResult};
use crate::services::screenshot_service::ScreenshotService;

/// 截取屏幕命令
///
/// # 参数
///
/// * `config` - 截图配置
///
/// # 返回
///
/// 返回截图结果
///
/// # 前端调用示例
///
/// ```typescript
/// import { invoke } from '@tauri-apps/api/tauri';
///
/// const result = await invoke('capture_screen', {
///   config: {
///     include_cursor: false,
///     quality: 90
///   }
/// });
/// console.log(result.data); // Base64 图片数据
/// ```
#[tauri::command]
pub fn capture_screen(config: ScreenshotConfig) -> Result<ScreenshotResult, String> {
    ScreenshotService::capture(&config).map_err(|e| e.to_string())
}

/// 截取区域命令
#[tauri::command]
pub fn capture_region(
    x: u32,
    y: u32,
    width: u32,
    height: u32,
    config: ScreenshotConfig,
) -> Result<ScreenshotResult, String> {
    ScreenshotService::capture_region(x, y, width, height, config)
        .map_err(|e| e.to_string())
}

最佳实践

1. 命名规范

类型 命名风格 示例
模块 snake_case color_service.rs
结构体 PascalCase ColorInfo, NewFeatureService
函数 snake_case get_pixel_color, toggle_window
常量 SCREAMING_SNAKE_CASE MAX_RETRY_COUNT
Trait PascalCase + 能力 ScreenAccessor, CursorController
类型别名 PascalCase + Type AppResult, JsonResult

2. 文档注释规范

/// 简短描述(一句话)
///
/// 详细描述(可以多行,说明功能、用途、注意事项)
///
/// # 参数
///
/// * `param1` - 参数说明
/// * `param2` - 参数说明
///
/// # 返回
///
/// 返回值说明
///
/// # 错误
///
/// - 错误类型1错误场景
/// - 错误类型2错误场景
///
/// # 示例
///
/// ```
/// let result = function_name(args);
/// assert!(result.is_ok());
/// ```
pub fn function_name(param1: Type1, param2: Type2) -> AppResult<ReturnType> {
    // 实现
}

3. 错误处理规范

// ✅ 推荐:使用具体的错误类型
fn do_something() -> AppResult<()> {
    if condition {
        return Err(AppError::InvalidData("数据无效".to_string()));
    }
    Ok(())
}

// ❌ 不推荐:直接返回 String
fn do_something() -> Result<(), String> {
    if condition {
        return Err("错误".to_string());
    }
    Ok(())
}

4. 参数验证规范

// ✅ 在 Service 层验证参数
impl SomeService {
    pub fn execute(&self, input: &str) -> AppResult<Output> {
        // 参数验证
        if input.is_empty() {
            return Err(AppError::InvalidData("输入不能为空".to_string()));
        }

        if input.len() > 1000 {
            return Err(AppError::InvalidData("输入过长".to_string()));
        }

        // 业务逻辑
        // ...
    }
}

5. 异步处理规范

// ✅ 提供 sync 和 async 两个版本
impl SomeService {
    // 同步版本
    pub fn execute(&self, input: &str) -> AppResult<Output> {
        // 直接调用
    }

    // 异步版本
    pub async fn execute_async(&self, input: String) -> AppResult<Output> {
        // 使用 async/await
        tokio::time::sleep(Duration::from_secs(1)).await;
        self.execute(&input)
    }
}

6. 代码组织规范

// ✅ 推荐:按功能分组
pub struct ColorService;

impl ColorService {
    // 公共方法
    pub fn new() -> Self { Self }
    pub fn do_work(&self) -> AppResult<()> { }

    // 私有辅助方法
    fn validate(&self) -> AppResult<()> { }
    fn process(&self) -> AppResult<()> { }
}

// ✅ 使用 mod 组织相关功能
mod color_conversion {
    pub fn rgb_to_hsl() { }
    pub fn hsl_to_rgb() { }
}

测试规范

单元测试

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_normal_case() {
        // 测试正常场景
        let result = function_under_test(normal_input);
        assert_eq!(result, expected);
    }

    #[test]
    fn test_edge_case() {
        // 测试边界条件
        let result = function_under_test(edge_input);
        assert!(result.is_ok());
    }

    #[test]
    fn test_error_case() {
        // 测试错误场景
        let result = function_under_test(invalid_input);
        assert!(result.is_err());
    }
}

集成测试

// tests/integration_test.rs

#[test]
fn test_full_workflow() {
    // 测试完整的业务流程
    let service = MyService::new();
    let config = MyConfig::default();

    let result = service.execute(&config).unwrap();
    assert_eq!(result.status, 200);
}

运行测试

# 运行所有测试
cargo test

# 运行特定测试
cargo test test_color_conversion

# 运行测试并显示输出
cargo test -- --nocapture

# 运行测试并生成文档
cargo test --doc

常见问题

Q: 如何添加新的错误类型?

A: 在 error.rs 中添加新的枚举变体,并在 Display 实现中添加处理:

pub enum AppError {
    // ...
    NewErrorType(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            // ...
            AppError::NewErrorType(msg) => write!(f, "新错误: {}", msg),
        }
    }
}

Q: 如何调用 Windows API

A: 在 Service 层直接使用 Windows 相关的 crate 或调用 Windows API

// 使用 windows-rs crate
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;

impl CursorService {
    pub fn get_cursor_position(&self) -> AppResult<(i32, i32)> {
        let mut pos = POINT { x: 0, y: 0 };
        unsafe {
            GetCursorPos(&mut pos)?;
        }
        Ok((pos.x, pos.y))
    }
}

Q: 如何处理异步操作?

A: 在 Service 层提供 async 函数Command 层使用 async 命令:

// Service
pub async fn fetch_data(&self) -> AppResult<Data> {
    // 异步操作
}

// Command
#[tauri::command]
pub async fn get_data() -> Result<Data, String> {
    service.fetch_data().await.map_err(|e| e.to_string())
}

检查清单

在提交代码前,确保:

  • 代码符合 Rust 格式规范(cargo fmt --check
  • 代码通过 clippy 检查(cargo clippy
  • 所有测试通过(cargo test
  • 编译成功(cargo check
  • 所有公开 API 都有文档注释
  • 文档注释包含示例代码
  • 错误消息使用中文
  • 遵循依赖原则(上层依赖下层)
  • 新增类型已导出(如需要)
  • 命令已在 lib.rs 中注册

参考资源