项目实践
在本章中,我们将把前面学到的所有 Rust 知识整合起来,构建一个完整的应用程序。通过实际项目,你将学习如何组织代码、处理依赖关系、实现功能以及测试和部署 Rust 应用程序。我们将构建一个命令行待办事项管理器(Todo CLI),它具有添加、列出、完成和删除任务的功能。
项目规划
在开始编码之前,让我们先规划项目的功能和结构。
功能需求
我们的待办事项管理器应该支持以下功能:
- 添加新任务
- 列出所有任务(可选按状态过滤)
- 将任务标记为已完成
- 删除任务
- 将任务保存到文件中,并在启动时加载
技术选择
我们将使用以下库来构建应用程序:
clap
:用于命令行参数解析serde
和serde_json
:用于 JSON 序列化和反序列化chrono
:用于处理日期和时间anyhow
:用于错误处理colored
:用于终端彩色输出
项目结构
todo-cli/
├── Cargo.toml
├── src/
│ ├── main.rs # 程序入口点
│ ├── cli.rs # 命令行接口
│ ├── task.rs # 任务数据结构
│ └── storage.rs # 文件存储逻辑
└── README.md
项目初始化
首先,让我们创建一个新的 Rust 项目:
cargo new todo-cli
cd todo-cli
然后,添加所需的依赖项到 Cargo.toml
文件:
[package]
name = "todo-cli"
version = "0.1.0"
edition = "2021"[dependencies]
clap = { version = "4.0", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1.0"
colored = "2.0"
实现任务数据结构
首先,我们需要定义任务的数据结构。创建 src/task.rs
文件:
use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use std::fmt;
use colored::*;#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Task {pub id: usize,pub description: String,pub completed: bool,pub created_at: DateTime<Local>,
}impl Task {pub fn new(id: usize, description: String) -> Self {Self {id,description,completed: false,created_at: Local::now(),}}pub fn toggle_completion(&mut self) {self.completed = !self.completed;}
}impl fmt::Display for Task {fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {let status = if self.completed {"[✓]".green()} else {"[ ]".red()};write!(f,"{} {} - {} ({})",status,self.id.to_string().blue(),self.description,self.created_at.format("%Y-%m-%d %H:%M"))}
}
实现存储逻辑
接下来,我们需要实现任务的存储和加载功能。创建 src/storage.rs
文件:
use crate::task::Task;
use anyhow::{Context, Result};
use std::fs::{self, File};
use std::io::{BufReader, BufWriter};
use std::path::Path;pub struct Storage {file_path: String,tasks: Vec<Task>,next_id: usize,
}impl Storage {pub fn new(file_path: &str) -> Result<Self> {let tasks = Self::load_tasks(file_path)?;let next_id = tasks.iter().map(|task| task.id).max().unwrap_or(0) + 1;Ok(Self {file_path: file_path.to_string(),tasks,next_id,})}fn load_tasks(file_path: &str) -> Result<Vec<Task>> {let path = Path::new(file_path);if !path.exists() {return Ok(Vec::new());}let file = File::open(path).with_context(|| format!("无法打开文件: {}", file_path))?;let reader = BufReader::new(file);let tasks = serde_json::from_reader(reader).with_context(|| format!("无法解析文件: {}", file_path))?;Ok(tasks)}fn save_tasks(&self) -> Result<()> {// 确保目录存在if let Some(parent) = Path::new(&self.file_path).parent() {fs::create_dir_all(parent).with_context(|| format!("无法创建目录: {}", parent.display()))?;}let file = File::create(&self.file_path).with_context(|| format!("无法创建文件: {}", self.file_path))?;let writer = BufWriter::new(file);serde_json::to_writer_pretty(writer, &self.tasks).with_context(|| format!("无法写入文件: {}", self.file_path))?;Ok(())}pub fn add_task(&mut self, description: String) -> Result<&Task> {let task = Task::new(self.next_id, description);self.next_id += 1;self.tasks.push(task);self.save_tasks()?;Ok(self.tasks.last().unwrap())}pub fn list_tasks(&self, show_completed: bool) -> Vec<&Task> {self.tasks.iter().filter(|task| show_completed || !task.completed).collect()}pub fn complete_task(&mut self, id: usize) -> Result<()> {let task = self.tasks.iter_mut().find(|task| task.id == id).with_context(|| format!("未找到 ID 为 {} 的任务", id))?;task.toggle_completion();self.save_tasks()?;Ok(())}pub fn delete_task(&mut self, id: usize) -> Result<Task> {let position = self.tasks.iter().position(|task| task.id == id).with_context(|| format!("未找到 ID 为 {} 的任务", id))?;let task = self.tasks.remove(position);self.save_tasks()?;Ok(task)}
}
实现命令行接口
现在,我们需要实现命令行接口。创建 src/cli.rs
文件:
use clap::{Parser, Subcommand};#[derive(Parser)]
#[clap(name = "todo")]
#[clap(about = "一个简单的待办事项管理器", long_about = None)]
pub struct Cli {#[clap(subcommand)]pub command: Commands,/// 任务文件的路径#[clap(short, long, default_value = "~/.todo.json")]pub file: String,
}#[derive(Subcommand)]
pub enum Commands {/// 添加一个新任务Add {/// 任务描述#[clap(required = true)]description: Vec<String>,},/// 列出所有任务List {/// 是否显示已完成的任务#[clap(short, long)]all: bool,},/// 将任务标记为已完成Complete {/// 任务 ID#[clap(required = true)]id: usize,},/// 删除任务Delete {/// 任务 ID#[clap(required = true)]id: usize,},
}
实现主程序
最后,我们需要实现主程序,将所有组件连接起来。更新 src/main.rs
文件:
mod cli;
mod storage;
mod task;use anyhow::{Context, Result};
use clap::Parser;
use cli::{Cli, Commands};
use colored::*;
use storage::Storage;
use std::path::PathBuf;fn main() -> Result<()> {let cli = Cli::parse();// 处理 ~ 路径let file_path = if cli.file.starts_with("~") {let home = dirs::home_dir().context("无法确定主目录")?;let path = cli.file.strip_prefix("~").unwrap();let mut path_buf = PathBuf::from(home);path_buf.push(path.trim_start_matches('/'));path_buf.to_string_lossy().to_string()} else {cli.file};let mut storage = Storage::new(&file_path)?;match cli.command {Commands::Add { description } => {let desc = description.join(" ");let task = storage.add_task(desc)?;println!("{} {}", "已添加任务:".green(), task);}Commands::List { all } => {let tasks = storage.list_tasks(all);if tasks.is_empty() {println!("{}", "没有任务".yellow());return Ok(());}println!("{}", "任务列表:".green());for task in tasks {println!("{}", task);}}Commands::Complete { id } => {storage.complete_task(id)?;println!("{} {}", "已完成任务 ID:".green(), id);}Commands::Delete { id } => {let task = storage.delete_task(id)?;println!("{} {}", "已删除任务:".green(), task);}}Ok(())
}
别忘了添加 dirs
依赖项到 Cargo.toml
:
[dependencies]
dirs = "4.0"
构建和运行
现在,我们可以构建和运行我们的待办事项管理器:
cargo build --release
使用示例:
# 添加任务
./target/release/todo-cli add 完成 Rust 项目实践章节# 列出任务
./target/release/todo-cli list# 列出所有任务(包括已完成)
./target/release/todo-cli list --all# 完成任务
./target/release/todo-cli complete 1# 删除任务
./target/release/todo-cli delete 1
项目扩展
现在我们有了一个基本的待办事项管理器,但还有很多方面可以改进和扩展:
1. 添加优先级
我们可以为任务添加优先级字段,并允许用户按优先级排序:
// 在 Task 结构体中添加
pub enum Priority {Low,Medium,High,
}// 添加排序功能
pub fn list_tasks_by_priority(&self) -> Vec<&Task> {let mut tasks = self.list_tasks(false);tasks.sort_by(|a, b| b.priority.cmp(&a.priority));tasks
}
2. 添加截止日期
我们可以为任务添加截止日期,并提供查看即将到期任务的功能:
// 在 Task 结构体中添加
pub due_date: Option<DateTime<Local>>,// 添加查看即将到期任务的功能
pub fn list_upcoming_tasks(&self, days: i64) -> Vec<&Task> {let now = Local::now();let deadline = now + chrono::Duration::days(days);self.tasks.iter().filter(|task| {if let Some(due_date) = task.due_date {!task.completed && due_date <= deadline} else {false}}).collect()
}
3. 添加标签
我们可以为任务添加标签,并允许用户按标签过滤:
// 在 Task 结构体中添加
pub tags: Vec<String>,// 添加按标签过滤的功能
pub fn list_tasks_by_tag(&self, tag: &str) -> Vec<&Task> {self.tasks.iter().filter(|task| task.tags.contains(&tag.to_string())).collect()
}
4. 添加数据库支持
我们可以使用 SQLite 或其他数据库替代 JSON 文件存储,以提高性能和可靠性:
// 使用 rusqlite 库
use rusqlite::{params, Connection, Result};pub struct DbStorage {conn: Connection,
}impl DbStorage {pub fn new(db_path: &str) -> Result<Self> {let conn = Connection::open(db_path)?;conn.execute("CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY,description TEXT NOT NULL,completed BOOLEAN NOT NULL DEFAULT 0,created_at TEXT NOT NULL)",[],)?;Ok(Self { conn })}// 实现其他方法...
}
5. 添加 Web 界面
我们可以使用 Rocket 或 Actix Web 框架添加一个 Web 界面,使用户可以通过浏览器管理任务:
// 使用 Rocket 框架
#[macro_use] extern crate rocket;#[get("/tasks")]
fn list_tasks() -> Json<Vec<Task>> {let storage = Storage::new("~/.todo.json").unwrap();Json(storage.list_tasks(true).into_iter().cloned().collect())
}#[post("/tasks", data = "<task>")]
fn add_task(task: Json<NewTask>) -> Json<Task> {let mut storage = Storage::new("~/.todo.json").unwrap();let new_task = storage.add_task(task.description.clone()).unwrap();Json(new_task.clone())
}#[launch]
fn rocket() -> _ {rocket::build().mount("/api", routes![list_tasks, add_task])
}
最佳实践
在开发这个项目的过程中,我们应用了许多 Rust 最佳实践:
1. 错误处理
我们使用 anyhow
库进行错误处理,提供了详细的错误上下文信息,使调试更容易。
2. 模块化设计
我们将代码分为多个模块(task.rs
、storage.rs
、cli.rs
),每个模块负责特定的功能,使代码更易于维护。
3. 使用适当的类型
我们使用了适当的类型来表示数据,如 DateTime<Local>
表示时间,usize
表示 ID,这有助于防止类型错误。
4. 文档和注释
我们为代码添加了文档注释,使其他开发者更容易理解代码的功能和用法。
5. 测试
虽然我们没有展示测试代码,但在实际项目中,应该为每个模块编写单元测试和集成测试,确保代码的正确性。
练习题
-
实现上述扩展功能之一(优先级、截止日期或标签)。
-
添加一个新命令
edit
,允许用户编辑现有任务的描述。 -
实现任务的导入和导出功能,支持 CSV 格式。
-
添加一个统计功能,显示已完成和未完成任务的数量,以及完成率。
-
将存储逻辑改为使用 SQLite 数据库,而不是 JSON 文件。
总结
在本章中,我们构建了一个完整的命令行待办事项管理器,应用了前面章节学到的 Rust 知识:
- 使用结构体和枚举表示数据
- 实现特质(如
Display
)自定义行为 - 使用
serde
进行序列化和反序列化 - 使用
clap
解析命令行参数 - 使用
anyhow
进行错误处理 - 组织代码为多个模块
通过这个项目,你应该对如何在实际应用中使用 Rust 有了更深入的理解。记住,最好的学习方法是实践,所以尝试完成练习题,或者扩展这个项目,添加你自己想要的功能。
恭喜你完成了 Rust 进阶篇的学习!现在你已经掌握了 Rust 的核心概念和实践技能,可以开始构建自己的 Rust 项目了。继续探索 Rust 生态系统,参与开源项目,不断提升你的 Rust 编程能力!