第二十章:项目实战——构建一个完整应用
本章导读:学习了所有基础知识后,是时候将它们整合成一个完整的项目了。本章我们将构建一个"任务管理 CLI 工具"——从需求分析到最终发布,体验完整的 Rust 项目开发流程。这不仅仅是一个练习,更是一次将知识转化为技能的旅程。
📋 20.1 项目规划
🎯 20.1.1 需求分析
我们要构建一个名为 taskr 的命令行任务管理工具:
核心功能:
- 添加任务
- 列出任务
- 完成任务
- 删除任务
- 持久化存储
进阶功能:
- 优先级
- 截止日期
- 标签
- 搜索过滤
📁 20.1.2 项目结构
taskr/
├── Cargo.toml
├── README.md
├── src/
│ ├── main.rs
│ ├── cli.rs
│ ├── task.rs
│ ├── storage.rs
│ └── error.rs
└── tests/
└── integration_test.rs
📦 20.2 项目初始化
cargo new taskr
cd taskr
# Cargo.toml
[package]
name = "taskr"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <your@email.com>"]
description = "A simple task management CLI tool"
license = "MIT"
[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }
colored = "2"
anyhow = "1"
thiserror = "1"
[dev-dependencies]
tempfile = "3"
🛠️ 20.3 核心模块实现
❌ 20.3.1 错误处理
// src/error.rs
use thiserror::Error;
#[derive(Error, Debug)]
pub enum TaskError {
#[error("任务未找到: {0}")]
NotFound(String),
#[error("无效的优先级: {0}")]
InvalidPriority(String),
#[error("存储错误: {0}")]
Storage(#[from] std::io::Error),
#[error("解析错误: {0}")]
Parse(#[from] serde_json::Error),
}
pub type Result<T> = std::result::Result<T, TaskError>;
📝 20.3.2 任务模型
// src/task.rs
use chrono::{DateTime, Local, Utc};
use serde::{Deserialize, Serialize};
/// 任务优先级
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Priority {
Low,
Medium,
High,
}
impl Default for Priority {
fn default() -> Self {
Self::Medium
}
}
impl std::fmt::Display for Priority {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Priority::Low => write!(f, "低"),
Priority::Medium => write!(f, "中"),
Priority::High => write!(f, "高"),
}
}
}
/// 任务状态
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Status {
Todo,
Done,
}
impl Default for Status {
fn default() -> Self {
Self::Todo
}
}
/// 任务
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
pub id: String,
pub title: String,
pub description: Option<String>,
pub priority: Priority,
pub status: Status,
pub tags: Vec<String>,
pub due_date: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Task {
/// 创建新任务
pub fn new(title: String) -> Self {
let now = Utc::now();
Self {
id: generate_id(),
title,
description: None,
priority: Priority::default(),
status: Status::default(),
tags: Vec::new(),
due_date: None,
created_at: now,
updated_at: now,
}
}
/// 设置描述
pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self.touch();
self
}
/// 设置优先级
pub fn with_priority(mut self, priority: Priority) -> Self {
self.priority = priority;
self.touch();
self
}
/// 设置截止日期
pub fn with_due_date(mut self, due_date: DateTime<Utc>) -> Self {
self.due_date = Some(due_date);
self.touch();
self
}
/// 添加标签
pub fn add_tag(&mut self, tag: String) {
if !self.tags.contains(&tag) {
self.tags.push(tag);
self.touch();
}
}
/// 标记完成
pub fn complete(&mut self) {
self.status = Status::Done;
self.touch();
}
/// 标记未完成
pub fn uncomplete(&mut self) {
self.status = Status::Todo;
self.touch();
}
/// 更新时间戳
fn touch(&mut self) {
self.updated_at = Utc::now();
}
}
/// 生成唯一 ID
fn generate_id() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap();
format!("{:x}", duration.as_nanos())
}
💾 20.3.3 存储模块
// src/storage.rs
use crate::error::{Result, TaskError};
use crate::task::Task;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
/// 任务存储
pub struct Storage {
path: PathBuf,
tasks: HashMap<String, Task>,
}
impl Storage {
/// 创建存储实例
pub fn new() -> Result<Self> {
let path = Self::default_path();
Self::with_path(path)
}
/// 使用指定路径
pub fn with_path(path: PathBuf) -> Result<Self> {
let tasks = if path.exists() {
let content = fs::read_to_string(&path)?;
serde_json::from_str(&content)?
} else {
HashMap::new()
};
Ok(Self { path, tasks })
}
/// 获取默认存储路径
fn default_path() -> PathBuf {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| ".".to_string());
Path::new(&home).join(".taskr.json")
}
/// 添加任务
pub fn add(&mut self, task: Task) -> Result<()> {
self.tasks.insert(task.id.clone(), task);
self.save()
}
/// 获取任务
pub fn get(&self, id: &str) -> Option<&Task> {
self.tasks.get(id)
}
/// 获取可变任务
pub fn get_mut(&mut self, id: &str) -> Option<&mut Task> {
self.tasks.get_mut(id)
}
/// 列出所有任务
pub fn list(&self) -> Vec<&Task> {
self.tasks.values().collect()
}
/// 删除任务
pub fn remove(&mut self, id: &str) -> Result<bool> {
let removed = self.tasks.remove(id).is_some();
if removed {
self.save()?;
}
Ok(removed)
}
/// 保存到文件
pub fn save(&self) -> Result<()> {
let content = serde_json::to_string_pretty(&self.tasks)?;
fs::write(&self.path, content)?;
Ok(())
}
/// 任务数量
pub fn len(&self) -> usize {
self.tasks.len()
}
/// 是否为空
pub fn is_empty(&self) -> bool {
self.tasks.is_empty()
}
}
💻 20.3.4 CLI 接口
// src/cli.rs
use clap::{Parser, Subcommand};
use chrono::{DateTime, Utc};
#[derive(Parser)]
#[command(name = "taskr")]
#[command(about = "任务管理 CLI 工具", long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
/// 添加新任务
Add {
/// 任务标题
title: String,
/// 任务描述
#[arg(short, long)]
description: Option<String>,
/// 优先级 (low/medium/high)
#[arg(short = 'P', long, default_value = "medium")]
priority: String,
/// 截止日期 (YYYY-MM-DD)
#[arg(short, long)]
due: Option<String>,
/// 标签
#[arg(short, long)]
tags: Vec<String>,
},
/// 列出任务
List {
/// 仅显示未完成
#[arg(short, long)]
todo: bool,
/// 仅显示已完成
#[arg(short, long)]
done: bool,
/// 按标签过滤
#[arg(short, long)]
tag: Option<String>,
/// 搜索关键词
#[arg(short, long)]
search: Option<String>,
},
/// 完成任务
Done {
/// 任务 ID
id: String,
},
/// 取消完成
Undo {
/// 任务 ID
id: String,
},
/// 删除任务
Remove {
/// 任务 ID
id: String,
},
/// 编辑任务
Edit {
/// 任务 ID
id: String,
/// 新标题
#[arg(short, long)]
title: Option<String>,
/// 新描述
#[arg(short, long)]
description: Option<String>,
/// 新优先级
#[arg(short = 'P', long)]
priority: Option<String>,
},
/// 清除已完成任务
Clean,
}
/// 解析优先级
pub fn parse_priority(s: &str) -> Option<crate::task::Priority> {
match s.to_lowercase().as_str() {
"low" | "l" | "低" => Some(crate::task::Priority::Low),
"medium" | "m" | "中" => Some(crate::task::Priority::Medium),
"high" | "h" | "高" => Some(crate::task::Priority::High),
_ => None,
}
}
/// 解析日期
pub fn parse_date(s: &str) -> Option<DateTime<Utc>> {
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
.ok()
.map(|d| d.and_hms_opt(23, 59, 59).unwrap().and_utc())
}
🎨 20.3.5 主程序
// src/main.rs
mod cli;
mod error;
mod storage;
mod task;
use crate::cli::{Cli, Commands, parse_date, parse_priority};
use crate::error::Result;
use crate::storage::Storage;
use crate::task::{Priority, Status, Task};
use clap::Parser;
use colored::Colorize;
fn main() -> Result<()> {
let cli = Cli::parse();
let mut storage = Storage::new()?;
match cli.command {
Commands::Add {
title,
description,
priority,
due,
tags,
} => {
let priority = parse_priority(&priority)
.unwrap_or(Priority::Medium);
let mut task = Task::new(title);
if let Some(desc) = description {
task = task.with_description(desc);
}
task = task.with_priority(priority);
if let Some(due_str) = due {
if let Some(due_date) = parse_date(&due_str) {
task = task.with_due_date(due_date);
}
}
for tag in tags {
task.add_tag(tag);
}
let id = task.id.clone();
storage.add(task)?;
println!("{} {}", "✓ 任务已创建:".green(), id.cyan());
}
Commands::List {
todo,
done,
tag,
search,
} => {
let tasks = storage.list();
let filtered: Vec<_> = tasks
.into_iter()
.filter(|t| {
// 状态过滤
if todo && t.status != Status::Todo {
return false;
}
if done && t.status != Status::Done {
return false;
}
// 标签过滤
if let Some(ref tag_filter) = tag {
if !t.tags.contains(tag_filter) {
return false;
}
}
// 搜索过滤
if let Some(ref search_term) = search {
let search_lower = search_term.to_lowercase();
if !t.title.to_lowercase().contains(&search_lower) {
return false;
}
}
true
})
.collect();
if filtered.is_empty() {
println!("{}", "没有找到任务".yellow());
} else {
println!("\n{}", "任务列表".bold());
println!("{}", "─".repeat(60).dimmed());
for task in filtered {
print_task(task);
}
}
}
Commands::Done { id } => {
if let Some(task) = storage.get_mut(&id) {
task.complete();
storage.save()?;
println!("{} {}", "✓ 任务已完成:".green(), task.title.cyan());
} else {
println!("{} {}", "✗ 任务未找到:".red(), id);
}
}
Commands::Undo { id } => {
if let Some(task) = storage.get_mut(&id) {
task.uncomplete();
storage.save()?;
println!("{} {}", "✓ 任务已恢复:".green(), task.title.cyan());
} else {
println!("{} {}", "✗ 任务未找到:".red(), id);
}
}
Commands::Remove { id } => {
if storage.remove(&id)? {
println!("{} {}", "✓ 任务已删除:".green(), id.cyan());
} else {
println!("{} {}", "✗ 任务未找到:".red(), id);
}
}
Commands::Edit {
id,
title,
description,
priority,
} => {
if let Some(task) = storage.get_mut(&id) {
if let Some(new_title) = title {
task.title = new_title;
}
if let Some(new_desc) = description {
task.description = Some(new_desc);
}
if let Some(new_priority) = priority {
if let Some(p) = parse_priority(&new_priority) {
task.priority = p;
}
}
storage.save()?;
println!("{} {}", "✓ 任务已更新:".green(), task.title.cyan());
} else {
println!("{} {}", "✗ 任务未找到:".red(), id);
}
}
Commands::Clean => {
let tasks = storage.list();
let done_ids: Vec<_> = tasks
.iter()
.filter(|t| t.status == Status::Done)
.map(|t| t.id.clone())
.collect();
let count = done_ids.len();
for id in done_ids {
storage.remove(&id)?;
}
println!("{} {} 个已完成任务", "✓ 已清理:".green(), count);
}
}
Ok(())
}
/// 打印任务
fn print_task(task: &Task) {
let status_icon = match task.status {
Status::Todo => "○".yellow(),
Status::Done => "●".green(),
};
let priority_icon = match task.priority {
Priority::High => "!!!".red(),
Priority::Medium => "!!".yellow(),
Priority::Low => "!".dimmed(),
};
let title = if task.status == Status::Done {
task.title.dimmed().strikethrough()
} else {
task.title.normal()
};
println!(
"{} {} {} {}",
status_icon,
title,
priority_icon,
task.id.dimmed()
);
if let Some(ref desc) = task.description {
println!(" {}", desc.dimmed());
}
if !task.tags.is_empty() {
let tags: String = task.tags.iter().map(|t| format!("#{}", t)).collect::<Vec<_>>().join(" ");
println!(" {}", tags.blue());
}
if let Some(ref due) = task.due_date {
let local = due.with_timezone(&chrono::Local);
let is_overdue = due < &Utc::now() && task.status == Status::Todo;
let due_str = format!("截止: {}", local.format("%Y-%m-%d"));
if is_overdue {
println!(" {}", due_str.red());
} else {
println!(" {}", due_str.dimmed());
}
}
}
🧪 20.4 测试
// tests/integration_test.rs
use std::process::Command;
fn taskr() -> Command {
Command::new("./target/debug/taskr")
}
#[test]
fn test_add_and_list() {
// 添加任务
let output = taskr()
.args(["add", "测试任务", "-P", "high"])
.output()
.expect("执行失败");
assert!(output.status.success());
// 列出任务
let output = taskr()
.args(["list"])
.output()
.expect("执行失败");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("测试任务"));
}
📦 20.5 发布
🔨 20.5.1 构建发布版本
cargo build --release
📋 20.5.2 编写 README
# taskr
一个简洁高效的命令行任务管理工具。
## 安装
cargo install taskr
## 使用
### 添加任务
taskr add "完成报告" -P high -d 2024-12-31 --tags work urgent
### 列出任务
taskr list taskr list --todo taskr list --tag work
### 完成任务
taskr done
### 删除任务
taskr remove
📝 本章小结
恭喜你完成了《Rust语言从入门到精通》的学习!
本章我们构建了一个完整的 CLI 工具,涵盖了:
- 需求分析:明确项目目标
- 模块设计:职责分离
- 错误处理:使用
thiserror - 数据持久化:JSON 存储
- CLI 接口:使用
clap - 测试:单元测试和集成测试
🎓 结语:Rust 之路
二十章的学习,从 Hello World 到完整项目,你已经掌握了 Rust 的核心知识:
基础篇:所有权、借用、类型系统 进阶篇:错误处理、模块、迭代器、生命周期、智能指针 高级篇:并发、异步、宏、Unsafe、类型系统高级特性 实战篇:Web 开发、CLI 工具、测试、嵌入式
Rust 的学习曲线陡峭,但收获丰厚。继续保持好奇心,不断实践,你会成为一名优秀的 Rustacean!
费曼技巧:最好的学习是教别人。尝试向朋友解释所有权系统——如果你能说清楚,你就真正理解了。
Happy Coding! 🦀