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

1019 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/` 中定义:
```rust
// 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/` 中实现:
```rust
// 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 层)
```rust
// 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 层)
```rust
// 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.rs``lib.rs`
```rust
// 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` 中添加:
```rust
// 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 类型定义:
```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编写测试
```rust
// 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验证和测试
```bash
# 1. 检查代码编译
cargo check
# 2. 运行测试
cargo test
# 3. 构建应用
pnpm build
# 4. 运行应用
pnpm tauri dev
# 5. 测试功能
# - 在前端界面测试新功能
# - 检查控制台输出
# - 验证错误处理
```
---
## 各层开发规范
### Models 层规范
**职责**:定义数据结构,不包含业务逻辑
**规范清单**
- ✅ 使用 `serde``Serialize``Deserialize`
- ✅ 实现 `Debug``Clone`
- ✅ 所有字段必须有文档注释
- ✅ 使用 `new()` 方法构造复杂对象
- ✅ 字段类型优先使用基本类型
- ✅ 集中定义在一个模块中
**示例**
```rust
/// 用户信息
#[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 操作
- ✅ 包含单元测试
- ✅ 使用清晰的函数命名
**示例**
```rust
/// 计算两点之间的距离
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 层函数
- ✅ 提供同步和异步版本(如需要)
- ✅ 使用详细的中文档注释
**示例**
```rust
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>`
- ✅ 参数使用结构体(便于扩展)
- ✅ 简洁的适配器代码
- ✅ 包含前端调用示例
- ✅ 错误转换为字符串
**示例**
```rust
#[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. 定义数据模型
```rust
// 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
```rust
// 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. 创建命令
```rust
// 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. 文档注释规范
```rust
/// 简短描述(一句话)
///
/// 详细描述(可以多行,说明功能、用途、注意事项)
///
/// # 参数
///
/// * `param1` - 参数说明
/// * `param2` - 参数说明
///
/// # 返回
///
/// 返回值说明
///
/// # 错误
///
/// - 错误类型1错误场景
/// - 错误类型2错误场景
///
/// # 示例
///
/// ```
/// let result = function_name(args);
/// assert!(result.is_ok());
/// ```
pub fn function_name(param1: Type1, param2: Type2) -> AppResult<ReturnType> {
// 实现
}
```
### 3. 错误处理规范
```rust
// ✅ 推荐:使用具体的错误类型
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. 参数验证规范
```rust
// ✅ 在 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. 异步处理规范
```rust
// ✅ 提供 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. 代码组织规范
```rust
// ✅ 推荐:按功能分组
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() { }
}
```
---
## 测试规范
### 单元测试
```rust
#[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());
}
}
```
### 集成测试
```rust
// 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);
}
```
### 运行测试
```bash
# 运行所有测试
cargo test
# 运行特定测试
cargo test test_color_conversion
# 运行测试并显示输出
cargo test -- --nocapture
# 运行测试并生成文档
cargo test --doc
```
---
## 常见问题
### Q: 如何添加新的错误类型?
A: 在 `error.rs` 中添加新的枚举变体,并在 `Display` 实现中添加处理:
```rust
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
```rust
// 使用 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 命令:
```rust
// 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` 中注册
---
## 参考资源
- [Rust 官方文档](https://doc.rust-lang.org/)
- [Tauri 官方文档](https://tauri.app/)
- [Rust API 指南](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html)
- [Rust 命名规范](https://rust-lang.github.io/api-guidelines/naming.html)