Rust模块系统与crate发布实践:从私有项目到开源分享

Rust模块系统与crate发布实践:从私有项目到开源分享

cover

一、模块系统的困惑:mod、use、pub到底怎么组织

Rust的模块系统是我学Rust时最困惑的部分之一——不是概念难,而是"怎么做"不清晰。mod.rs和文件名的关系、use的路径规则、pub的可见性层级、mod声明和文件系统的对应——每个单独看都懂,组合起来就乱。

更困惑的是,什么时候该拆模块,什么时候该拆crate?模块和crate的边界在哪里?这些问题在教程里往往一笔带过,但实际写项目时天天遇到。

本文梳理Rust模块系统的核心规则,并记录我发布第一个crate的完整过程。

二、模块系统核心规则

2.1 模块声明与文件系统

graph TB
    A[src/lib.rs] --> B[mod scanner]
    A --> C[mod output]
    A --> D[mod config]
    B --> E[src/scanner.rs]
    C --> F[src/output.rs]
    D --> G[src/config.rs]
    E --> H[src/scanner/mod.rs 或 src/scanner.rs]
    B --> I[mod deep]
    I --> J[src/scanner/deep.rs]

2.2 模块声明的两种方式

// src/lib.rs

// 方式1:内联模块(小模块适合)
mod utils {
    pub fn format_size(bytes: u64) -> String {
        if bytes < 1024 {
            format!("{} B", bytes)
        } else {
            format!("{:.1} KB", bytes as f64 / 1024.0)
        }
    }
}

// 方式2:外部文件模块
mod scanner;   // 对应 src/scanner.rs 或 src/scanner/mod.rs
mod output;    // 对应 src/output.rs
mod config;    // 对应 src/config.rs

2.3 可见性规则

// 默认私有,需要pub才能被外部访问
mod internal {
    fn private_fn() {}        // 仅本模块可见
    pub fn public_fn() {}     // 本模块及父模块可见
    pub(crate) fn crate_fn() {}  // 整个crate可见
    pub(super) fn parent_fn() {} // 仅父模块可见
}

// 结构体字段也是私有的
pub struct Config {
    pub path: String,          // 公开字段
    max_depth: usize,          // 私有字段,外部不能直接访问
}

impl Config {
    pub fn new(path: String, max_depth: usize) -> Self {
        Self { path, max_depth }
    }

    pub fn max_depth(&self) -> usize {
        self.max_depth  // 通过方法暴露私有字段
    }
}

2.4 use与路径

// 绝对路径从crate根开始
use crate::scanner::FileScanner;

// 相对路径从当前模块开始
use super::config::AppConfig;  // 父模块
use self::utils::format_size;  // 当前模块

// 惯用法:函数用完整路径,类型用短路径
use std::collections::HashMap;
use std::fs::read_dir;  // 函数可以完整路径

// 重命名避免冲突
use std::io::Result as IoResult;
use anyhow::Result;

三、从模块到crate:拆分决策

3.1 何时拆成独立crate

graph TD
    A{是否被多个项目复用?} -->|是| B[拆成独立crate]
    A -->|否| C{是否需要独立版本?}
    C -->|是| B
    C -->|否| D{模块是否>500行?}
    D -->|是| E[拆成子模块]
    D -->|否| F[保持当前结构]

3.2 crate发布准备

Cargo.toml配置

[package]
name = "dust-scanner"           # crate名称,全局唯一
version = "0.1.0"               # 语义化版本
edition = "2021"
authors = ["Chen Yiming <yiming@example.com>"]
license = "MIT"                 # 必须指定license
description = "A disk usage scanner library"
repository = "https://github.com/example/dust-scanner"
keywords = ["disk", "scanner", "filesystem"]
categories = ["filesystem"]

[dependencies]
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }

[dev-dependencies]
tempfile = "3.8"  # 测试用临时目录

文档注释

/// 扫描指定目录,返回每个子目录的大小
///
/// # Arguments
///
/// * `path` - 要扫描的根目录路径
/// * `max_depth` - 最大递归深度
///
/// # Examples
///
/// ```
/// use dust_scanner::scan_directory;
///
/// let entries = scan_directory(".", 5).unwrap();
/// for entry in &entries {
///     println!("{}: {} bytes", entry.path.display(), entry.size);
/// }
/// ```
///
/// # Errors
///
/// 当路径不存在或不是目录时返回错误
pub fn scan_directory(
    path: &str,
    max_depth: usize,
) -> Result<Vec<DirEntry>> {
    // ...
}

3.3 测试组织

// 单元测试:和代码放在一起
pub fn format_size(bytes: u64) -> String {
    match bytes {
        0..1024 => format!("{} B", bytes),
        1024..1048576 => format!("{:.1} KB", bytes as f64 / 1024.0),
        _ => format!("{:.1} MB", bytes as f64 / 1048576.0),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_format_size_bytes() {
        assert_eq!(format_size(512), "512 B");
    }

    #[test]
    fn test_format_size_kb() {
        assert_eq!(format_size(2048), "2.0 KB");
    }

    #[test]
    fn test_format_size_mb() {
        assert_eq!(format_size(3 * 1048576), "3.0 MB");
    }
}
// 集成测试:tests/目录下
// tests/integration_test.rs
use dust_scanner::scan_directory;
use tempfile::TempDir;

#[test]
fn test_scan_empty_directory() {
    let tmp = TempDir::new().unwrap();
    let entries = scan_directory(
        tmp.path().to_str().unwrap(), 5
    ).unwrap();
    assert!(entries.is_empty());
}

#[test]
fn test_scan_with_files() {
    let tmp = TempDir::new().unwrap();
    std::fs::write(tmp.path().join("test.txt"), "hello").unwrap();

    let entries = scan_directory(
        tmp.path().to_str().unwrap(), 5
    ).unwrap();
    assert!(!entries.is_empty());
}

四、发布流程

4.1 发布前检查清单

# 1. 运行所有测试
cargo test --all

# 2. 检查文档
cargo doc --open

# 3. 运行Clippy
cargo clippy -- -D warnings

# 4. 检查格式
cargo fmt --check

# 5. 干跑发布(不实际发布)
cargo publish --dry-run

# 6. 检查包大小
cargo package --list

4.2 发布命令

# 首次登录crates.io
cargo login <api-token>

# 发布
cargo publish

# 版本更新后重新发布
# 修改Cargo.toml中的version
cargo publish

4.3 版本号规则

0.1.0 → 0.1.1  修复bug,不改变API
0.1.0 → 0.2.0  新增功能,可能改变API(0.x阶段不保证兼容)
1.0.0 → 1.1.0  新增功能,向后兼容
1.1.0 → 2.0.0  破坏性变更

五、架构权衡与边界分析

5.1 模块 vs crate

模块是编译单元内的代码组织,crate是独立的编译和发布单元。模块间零开销访问,crate间有API边界。建议:项目内用模块组织,跨项目复用才拆crate。过早拆crate会增加编译时间和维护成本。

5.2 文档注释的投入

文档注释写起来费时间,但对crate的可用性至关重要。建议:公开API必须有文档注释和示例代码,私有函数不需要。cargo doc生成的文档质量取决于注释的投入。

5.3 0.x版本的承诺

0.x版本意味着"API不稳定,随时可能变"。不要害怕在0.x阶段做破坏性变更,但要在CHANGELOG中记录。1.0之后再做破坏性变更就要慎重。

六、总结

Rust模块系统的核心规则:mod声明对应文件系统,pub控制可见性,use引入路径。模块是代码组织的基本单位,crate是发布和复用的基本单位。拆分决策的关键是"是否需要跨项目复用"。

发布crate的流程:写好文档注释→运行测试→Clippy检查→dry-run验证→正式发布。版本号遵循语义化版本规范,0.x阶段允许破坏性变更。

落地建议:项目初期用模块组织代码,稳定后再考虑拆crate;公开API必须写文档注释和示例;发布前用cargo publish --dry-run验证;0.x阶段大胆迭代,1.0之后再保证兼容性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值