Cargo工作区与多crate架构:从模块拆分到发布流程的工程实践
一、单crate的"膨胀诅咒":为什么10万行代码放在一个包里是灾难
Rust 项目从小到大,最常见的演进路径是:一个main.rs→ 一个src/目录 → 一个巨大的Cargo.toml。当代码量超过 5 万行,问题开始显现:编译时间从 30 秒涨到 5 分钟(改一行代码也要全量编译)、团队协作时cargo test经常因为别人的模块失败、版本发布只能整体发布无法独立升级。
Cargo Workspace(工作区)是解决这些问题的标准方案。它将一个项目拆分为多个 crate,共享一个Cargo.lock和target/目录,每个 crate 可以独立编译、测试和发布。但拆分本身不是目的——错误的拆分方式会导致循环依赖、版本地狱和发布噩梦。
二、Cargo工作区的架构与依赖关系
flowchart TB subgraph Workspace A[my-app: 二进制 crate] --> B[my-core: 核心库] A --> C[my-infra: 基础设施库] C --> B D[my-api: API 层] --> B D --> C E[my-cli: 命令行工具] --> B E --> C end subgraph 依赖方向规则 F[二进制 crate → 库 crate] --> G[上层 crate → 下层 crate] G --> H[禁止循环依赖] H --> I[核心 crate 零外部依赖] end subgraph 发布策略 J[my-core: 频繁发布] --> K[my-infra: 跟随 core 版本] K --> L[my-app: 稳定发布] end工作区的核心设计原则:依赖方向单向(上层依赖下层,禁止反向)、核心 crate 零外部依赖(减少供应链风险)、二进制 crate 只做组装不做逻辑。每个 crate 的职责边界通过 API 设计和版本号约束来保证。
三、Cargo工作区的工程实践
3.1 工作区配置与crate拆分
# 根目录 Cargo.toml —— 工作区配置 [workspace] resolver = "2" members = [ "crates/core", "crates/infra", "crates/api", "crates/cli", "crates/app", ] # 工作区级别的依赖版本统一管理 [workspace.dependencies] serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.35", features = ["full"] } tracing = "0.1" anyhow = "1.0" thiserror = "1.0" # 内部 crate 的版本统一 my-core = { path = "crates/core", version = "0.1.0" } my-infra = { path = "crates/infra", version = "0.1.0" }# crates/core/Cargo.toml —— 核心库,零外部依赖 [package] name = "my-core" version = "0.1.0" edition = "2021" [dependencies] # 核心库尽量减少外部依赖 serde = { workspace = true } thiserror = { workspace = true }# crates/infra/Cargo.toml —— 基础设施层 [package] name = "my-infra" version = "0.1.0" edition = "2021" [dependencies] my-core = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } anyhow = { workspace = true }3.2 核心库的API设计
// crates/core/src/lib.rs //! 核心库:定义领域模型和 trait,零业务逻辑 pub mod error; pub mod model; pub mod repository; // 重新导出核心类型,简化下游 crate 的导入路径 pub use error::CoreError; pub use model::{User, Order, Product}; pub use repository::Repository; // crates/core/src/model.rs use serde::{Deserialize, Serialize}; /// 用户模型 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct User { pub id: String, pub name: String, pub email: String, pub role: UserRole, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum UserRole { Admin, Member, Guest, } /// 订单模型 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Order { pub id: String, pub user_id: String, pub items: Vec<OrderItem>, pub status: OrderStatus, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OrderItem { pub product_id: String, pub quantity: u32, pub unit_price: f64, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum OrderStatus { Pending, Confirmed, Shipped, Delivered, Cancelled, } // crates/core/src/repository.rs use crate::error::CoreError; use crate::model::{User, Order}; /// 仓库 trait:定义数据访问接口 /// 下游 crate 提供具体实现(MySQL、Redis、内存等) pub trait Repository: Send + Sync { fn find_user_by_id(&self, id: &str) -> Result<Option<User>, CoreError>; fn save_user(&self, user: &User) -> Result<(), CoreError>; fn find_order_by_id(&self, id: &str) -> Result<Option<Order>, CoreError>; fn save_order(&self, order: &Order) -> Result<(), CoreError>; }3.3 基础设施层的实现
// crates/infra/src/lib.rs //! 基础设施层:提供 Repository 的具体实现 pub mod mysql_repo; pub mod cache; pub mod config; pub use mysql_repo::MySqlRepository; pub use cache::CacheLayer; // crates/infra/src/mysql_repo.rs use my_core::{Repository, CoreError, User, Order}; /// MySQL 实现 pub struct MySqlRepository { pool: sqlx::MySqlPool, } impl MySqlRepository { pub async fn new(database_url: &str) -> Result<Self, CoreError> { let pool = sqlx::MySqlPool::connect(database_url) .await .map_err(|e| CoreError::Infrastructure(e.to_string()))?; Ok(Self { pool }) } } impl Repository for MySqlRepository { fn find_user_by_id(&self, id: &str) -> Result<Option<User>, CoreError> { // 使用 sqlx 查询 // 注意:实际实现需要 async trait 或同步包装 todo!("实现 MySQL 查询") } fn save_user(&self, user: &User) -> Result<(), CoreError> { todo!("实现 MySQL 写入") } fn find_order_by_id(&self, id: &str) -> Result<Option<Order>, CoreError> { todo!("实现 MySQL 查询") } fn save_order(&self, order: &Order) -> Result<(), CoreError> { todo!("实现 MySQL 写入") } }3.4 发布流程与版本管理
// scripts/release.rs —— 自动化发布脚本 use std::process::Command; fn main() { // 1. 运行全量测试 run_command("cargo", &["test", "--workspace"]); // 2. 运行 clippy 检查 run_command("cargo", &["clippy", "--workspace", "--", "-D", "warnings"]); // 3. 按依赖顺序发布 crate let release_order = ["my-core", "my-infra", "my-api", "my-cli"]; for crate_name in &release_order { println!("发布 {}...", crate_name); // 检查是否有未提交的变更 let status = Command::new("git") .args(["status", "--porcelain", &format!("crates/{}", crate_name.replace("my-", ""))]) .output() .expect("执行 git status 失败"); if !status.stdout.is_empty() { panic!("{} 有未提交的变更,请先提交", crate_name); } // 发布到 crates.io run_command("cargo", &[ "publish", "-p", crate_name, "--dry-run", // 先试运行,确认无误后去掉 ]); println!("{} 发布完成", crate_name); } } fn run_command(program: &str, args: &[&str]) { let status = Command::new(program) .args(args) .status() .unwrap_or_else(|e| panic!("执行 {} 失败: {}", program, e)); if !status.success() { panic!("命令执行失败: {} {}", program, args.join(" ")); } }四、工作区架构的边界条件与工程权衡
循环依赖的检测与避免:Cargo 禁止循环依赖,但间接循环可能通过 trait 实现隐式引入——A 的 trait 在 B 中实现,B 的 trait 在 A 中实现。解决方案是引入第三个 crate C 放置共享的 trait 定义。但这增加了 crate 数量和依赖复杂度。
版本协调的噩梦:工作区内 crate 的版本号需要协调——如果my-core升级到 0.2.0 且有破坏性变更,my-infra和my-api也需要同步升级。workspace.dependencies 统一管理版本号,但破坏性变更的影响范围仍需人工评估。建议使用语义化版本(semver)严格约束:补丁版本必须向后兼容。
编译时间的边际递减:工作区共享target/目录,增量编译只需重编译变更的 crate 及其依赖者。但当 crate 拆分过细(如每个模块一个 crate),编译器需要在每个 crate 边界做代码生成和优化,反而增加编译时间。经验值是:一个 crate 的代码量在 2000-10000 行时编译效率最优。
发布顺序的依赖约束:crate 发布到 crates.io 时,依赖的 crate 必须先发布。如果my-infra依赖my-core0.2.0,那么my-core0.2.0 必须先发布。这要求发布脚本按依赖拓扑排序执行。当发布失败时(如 crates.io 暂时不可用),需要回滚已发布的版本——但 crates.io 不支持删除已发布版本。
五、总结
Cargo 工作区通过多 crate 架构解决单 crate 膨胀问题:独立编译缩短编译时间、独立测试减少协作冲突、独立发布支持增量升级。核心设计原则:依赖方向单向(上层→下层)、核心 crate 零外部依赖、二进制 crate 只做组装。关键权衡:循环依赖需引入共享 crate、版本协调需人工评估破坏性变更、crate 拆分过细反而增加编译时间、发布顺序受依赖拓扑约束。落地建议:crate 代码量控制在 2000-10000 行;使用 workspace.dependencies 统一管理版本;发布脚本按依赖拓扑排序执行;核心 crate 尽量减少外部依赖;每次发布前运行全量测试和 clippy 检查。