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-core 0.2.0,那么 my-core 0.2.0 必须先发布。这要求发布脚本按依赖拓扑排序执行。当发布失败时(如 crates.io 暂时不可用),需要回滚已发布的版本——但 crates.io 不支持删除已发布版本。
五、总结
Cargo 工作区通过多 crate 架构解决单 crate 膨胀问题:独立编译缩短编译时间、独立测试减少协作冲突、独立发布支持增量升级。核心设计原则:依赖方向单向(上层→下层)、核心 crate 零外部依赖、二进制 crate 只做组装。关键权衡:循环依赖需引入共享 crate、版本协调需人工评估破坏性变更、crate 拆分过细反而增加编译时间、发布顺序受依赖拓扑约束。落地建议:crate 代码量控制在 2000-10000 行;使用 workspace.dependencies 统一管理版本;发布脚本按依赖拓扑排序执行;核心 crate 尽量减少外部依赖;每次发布前运行全量测试和 clippy 检查。
1863

被折叠的 条评论
为什么被折叠?



