R

Rust:项目结构示例逐步介绍结论

2025-06-08

Rust:项目结构示例分步说明

介绍

结论

介绍

Actix-web 最近(希望是暂时的)的死亡迫使我重新检查我的一个宠物项目的一些代码,该项目没有完全解耦,我的一些 Diesel 与我的 Actix 混合在一起,所以我将分享 Rust 中的分解过程和阶段。

本书对此进行了详尽的解释,但我会尽量使其更加简洁明了。为了清晰起见,我会省略许多细节。当然,代码本身并没有什么用处,它的唯一目的是尽可能简单地逐步展示整个过程。希望对大家有所帮助。

我还在 GitHub 上上传了一个代码库 ( https://github.com/robertorojasr/rust-split-example ),如果你克隆它,每次提交都会增加一个步骤。与这个例子不同的是,它会运行(虽然没做什么,但确实能运行)。

这种演变应该是这样的:

  1. 包含所有内容的单个文件。
  2. 将部分代码移至不同的模块。(此步骤在 repo 中缺失,我忘记了,起诉我吧),请不要起诉我。
  3. 将模块移动到另一个文件。
  4. 将单个文件中的模块变成一个文件夹,作为一个模块,其中有多个文件作为子模块。
  5. 将您的板条箱分成位于同一目录树中的库和可执行文件。
  6. 将可执行文件和库移动到具有其自己的目录树的不同包(工作区)。

当我们的项目变得更大、更复杂时,这些策略将有助于保持代码的整体结构易于管理、清晰和解耦。

0. 起点

因此,您创建了“Hello World”,一切都很酷,您替换了那个 println 并开始编码,很快您就注意到您可爱的 main.rs 变得越来越大,充满了结构、函数和特征,其中一些相互关联,但有些则不然,就像尼奥看黑客帝国一样,您开始看到一些宏观结构,然后您告诉自己,“是时候解决这个烂摊子了”,当然,您可以在开始之前设计好一切(实际上您应该这样做),但您要避免过度设计,正如这篇文章所示(至少希望如此),没有必要从一开始就采取强硬措施。

可以说这是你目前的杰作,令人印象深刻,我知道,这一切都是我自己完成的。

// src/main.rs

struct A {
    a: i32,
}

struct B {
    b: i32,
}

fn main() {
    let first = A { a: 42, };
}

1. 一个模块

那么你该如何开始呢?第一件事就是制作模块来封装一些代码

// src/main.rs

mod something {
    struct A {
        a: i32,
    }

    struct B {
        b: i32,
    }
}

fn main() {
    let first = A { a: 42, };
}

但现在有一个问题,即使A在同一个文件中定义,也不再在同一个模块中,所以main()不知道其中有东西,所以我们必须导入一些东西

// src/main.rs 

mod something {
    struct A {
        a: i32,
    }

    struct B {
        b: i32,
    }
}

use crate::something::*; // <- this is new

fn main() {
    let first = A { a: 42, };
}

现在main()知道这里有一些东西,并且它可以使用其中的所有公共内容,但是等等,其中没有任何公共内容,所以我们必须将我们想要让main()看到的所有内容公开。

// src/main.rs 

mod something {
    pub struct A { // <- this is new
        pub a: i32, // <- this is new
    }

    pub struct B {
        pub b: i32, // <- this is new
    }
}

use crate::something::*;

fn main() {
    let first = A { a: 42, };
}

现在main()可以看到里面的所有内容,当然你不必将所有内容都公开,只需公开你需要的内容即可,并且不要用 OOP 参数来表示你不应该公开数据,这只是一个例子,顺便说一下,OOP 并不是全部(这里有点无礼)。

2. 其他文件中的模块

但这并不能解决你的 IDE 因文件仍然很大而变得迟缓的问题,事实上你只是添加了更多的东西!为了解决这个问题你应该开始使用 Vim...我在开玩笑...(是吗?)

因此,现在您想将该模块移到外部,移到其他文件,以便自由且独立地生存。

因此,您将定义和导入保留在main.rs中

// src/main.rs

mod something;
// the content of the module was here

use crate::something::*;

fn main() {
    let first = A { a: 42, };
}

并将模块内容放入新创建的文件中,该文件与main.rs位于同一文件夹中

// src/something.rs 

pub struct A {
    pub a: i32,
}

pub struct B {
    pub b: i32,
}

当你在main.rs中添加模块时

// src/main.rs

mod something;

// the rest of it

Rust 会自动在文件中查找它,如果找不到,则在同一文件夹(在本例中为 src/)中查找具有模块名称的文件,如果仍然找不到,则查找具有模块名称和文件mod.rs 的文件夹,然后在那里查找代码。

3. 文件夹中的模块包含多个子模块

正如上一段提到的,我们可以进一步拆分我们的模块,
我们所要做的就是创建一个文件夹这样我们就可以得到:

src/
 |_ main.rs
 |_ something.rs
 |_ something/

现在,我们可以将something.rs重命名mod.rs,并将其移到something/中,但这样做有什么意义呢?我们想要拆分东西!所以我们要给AB分别赋予它们自己的模块(请不要在类似的例子之外这样命名你的东西)。这样我们就有了这个树。

src/
 |_ main.rs
 |_ something/
     |_ mod.rs
     |_ a.rs
     |_ b.rs

但是对于something.rs会发生什么呢,好吧我的朋友,你把它分开,A转到a.rs并且你可以猜出B去了哪里。

现在AB都在它们自己的模块中,因此我们相应地修改导入

// src/main.rs

use crate::something::a::*; // <- this is new
use crate::something::b::*; // <- this is new

fn main() {
    let first = A { a: 42, };
}

但是mod.rs现在有责任调用它们的孩子,正如我之前所说的,当 Rust 检查something.rs并且没有找到时,它会检查文件夹something,然后在里面寻找名为mod.rs的文件。

// src/something/mod.rs

pub mod a;
pub mod b;

你可能会注意到,这和我们一开始对 main 函数做的事情是一样的。你可以根据需要不断嵌套模块,就像这样。

// src/something/a.rs

pub struct A {
    pub a: i32,
}

// src/something/b.rs

pub struct B {
    pub b: i32,
}

很好,但是现在怎么办?如果我的项目很庞大,如果我可以重用部分代码怎么办?你之前有一个很棒的 Web 应用,使用了 Diesel ORM 和 Actix-web,但万一哪天 Actix 的创始人离职了,项目的未来变得不确定,那该怎么办?那该怎么办?!

...

好吧,您可以将您的 webapp 视为一个处理 DB 的库,比如说使用 Diesel 和 Actix-web 或其他框架的可执行文件中的单独消费者,这些框架显然没有消失,但可能会回来......

例如,您还可以创建一个使用相同 DB 相关代码库的 CLI UI。我们开始吧!

4. 带有可执行文件的 Rust 库

您可能已经阅读并跳过了更多有趣的内容,Rust 识别两种类型的板条箱(我一直称之为项目的正式名称,只是因为我是一个反叛者)库和可执行文件,您可能知道它们之间的区别,但为了完整起见,让我们简单一点,可执行文件是您直接使用的东西,而库是可执行文件使用的东西。

在这种情况下,我们想要将main()作为可执行文件放入另一个文件中,并将所有结构留在库中,以便将来在其他可执行文件中重用。

Rust 非常努力地让事情变得简单(因为它对所有与洞穴和生命周期相关的痛苦感到内疚),所以要将一个板条箱变成一个库,你只需将文件main.rs重命名为lib.rs,瞧,现在是一个库,但让我们用它做一些有用的东西,要做到这一点,我们将创建一个新的文件夹bin/,我们将复制我们现有的main.rs,(现在重命名为lib.rs)在上面,用一些花哨的描述性名称,给你留下这个新树:

src/
 |_ lib.rs // <- just a renamed main.rs
 |_ bin/ // <- this folder is new
 |_  |_ framework_that_broke_my_heart.rs // <- a copy of our ex-main.rs
 |_ something/
     |_ mod.rs
     |_ a.rs
     |_ b.rs   

所以,something/里面的所有内容现在都不会被触动,我们先来处理framework_that_broke_my_heart.rs。首先要注意的是,bin/里面的所有内容都处于一个泡沫宇宙中,即使我们 crate 里面的内容也不再是它的一部分了,就像我们和社会一样(不是你,适应良好的程序员……呃),所以我们必须调用我们新创建的库(你知道,当我们把main.rs重命名为lib.rs时),就像我们调用任何库一样。

// src/bin/framework_that_broke_my_heart.rs

extern crate this_example; // oh right, I never named
// this crate, is the name you give in Cargo.toml
// under [package] in tha *name* field
// (don't use dashes on it)

use crate::something::a::*;
use crate::something::b::*;

fn main() {
    let first = A { a: 42, };
}

并在lib.rs ex-main.rs中

// src/lib.rs

pub mod something; // <- this is all, is like telling Rust
// copy/paste everything inside `something` inside a `mod` here

物体内部的所有东西都未受影响。

我们已经接近终点线了,现在你可能想知道那个可怜的家伙的情况,他最喜欢的框架的早期消亡让他心碎,并告诉自己,他到底是怎么把所有的 Actix-web 代码放在一个很小的文件中,那个文件一定很大,很乱,但整个想法是分裂事物,他只是让一切变得更糟!好吧,伙计们,这就是下一个要点。

5. 工作区

这会将你放在一个类似大箱子的保护伞下的几个较小的箱子里,你现在可以将代码拆分成两个箱子了,毕竟,我们声明了后续步骤中的可执行文件已经不属于原始箱子了,没错。将代码完全拆分成两个独立的箱子是一个有效的选择,但许多依赖项对可执行文件和库都是通用的;而且重建两次、测试两次等等会很麻烦。如果库和可执行文件彼此相关,你可能希望将它们视为一个整体,以便进行构建/测试/运行,你也可以将它们保存在同一个仓库中。

这会变得有点复杂,但不会太复杂。

我们将在原来的箱子里制作两个箱子,然后将它们粘在一起。

于是我们就有了这样的美丽:

./
 |_Cargo.toml
 |_Cargo.lock
 |_target/
 | |_ ... // we don't care about this, is made in the building process
 |
 |_src/
   |_ lib.rs
   |_ bin/
   |_  |_ framework_that_broke_my_heart.rs
   |_ something/
       |_ mod.rs
       |_ a.rs
       |_ b.rs   

坐在“货物”旁边,我们刚刚制作了两个新板条箱。

$ cargo init --lib db_stuff
$ cargo init ftbmh // framework_that_broke_my_heart, too long,
// too lazy, again, this is an example, name your thing with
// common sense, don't be // funny in a real project, the fun
// will last about 10min, the pain much // more than that.

参数 --lib 唯一的作用是,改为生成main.rs并生成lib.rs,默认情况下生成可执行文件。

因此我们将得到:

./
 |_Cargo.toml
 |_Cargo.lock
 |_target/
 | |_ ...
 |
 |_src/
 |  |_ lib.rs
 |  |_ bin/
 |  |_  |_ framework_that_broke_my_heart.rs
 |  |_ something/
 |      |_ mod.rs
 |      |_ a.rs
 |      |_ b.rs
 |
 | // ^ that's the old part
 |
 |_db_stuff/    // this whole folder is new
 |   |_Cargo.toml
 |   |_src/
 |      |_ lib.rs // <- that's all the --lib does
 |_ftbmh/      // this whole folder is new
     |_Cargo.toml
     |_src/
        |_ main.rs

现在,我们将从原始的Cargo.toml中根据需要将部分内容移动到新的Cargo.toml中,例如将依赖项移动到需要它们的任何人。

完成后,只需清理旧的Cargo.toml并输入以下内容:

[workspace]
members = ["db_stuff", "ftbmh"]

就是这样,现在原始的Cargo.toml没有 [package] 或 [dependencies] 部分;原始板条箱现在是一个外壳。

当您在里面制作 2 个板条箱(dn_stuffftbmh)时,cargo 看到您位于一个现有的板条箱内,它有自己的 git 仓库,所以没有为它们制作一个,您的旧仓库仍然良好且健康。

现在请记住,您拆分了代码,并且可能一部分依赖于其他部分,在这种情况下,ftbmh依赖于db_stuff,因此我们必须在ftbmh Cargo.toml文件中添加该依赖项

// ftbmh/Cargo.tom

[package]
// your stuff

[dependencies]
// your dependencies
db_stuff = { path = "../db_stuff" } // as you may know the `..`
// in the path refers to the mother folder of the current one

您可能还记得上一步中ftbmh/main.rs已经在原始板条箱之外,因此一切都在那里完成,并且板条箱 db_stuff 已经被用作外部板条箱,所以那里的一切也都是一样的。

结论

我们完成了。最初只是一个简单的单文件项目,现在变成了一个包含 2 个工作区的复杂 crate,当然,工作区可能是 3 个,甚至可能是 100 个,只需重复上述步骤即可,Rust 命名空间中的模块也同样如此。

如你所见,如果你的代码正确解耦,整个过程会非常低调。当然,从一开始就规划好设计是件好事,但有时项目的发展会超出我们的预期,变得更加复杂,有方法轻松地进行调整而不造成混乱是件好事,也因为你不必为了担心未来可能出现的问题(例如本文中提到的情况)而过度设计你的解决方案。你可以有机地扩展你的代码。

希望以上内容对大家有所帮助。虽然这没什么新意,但我发现文档和书籍中的内容太过分散。相关内容还有很多,但我并没有过多讨论公共/私有属性以及默认哪些属性对哪些人可见,这些在本书和精彩的O'Reilly 的《Rust 编程》中都有很好的解释。我只是尝试搭建一个框架,方便以后轻松挂载细节。

任何更正和建议,请随时告诉我,特别是如果写得很奇怪,英语不是我的母语,这是我目前最好的。

鏂囩珷鏉ユ簮锛�https://dev.to/ghost/rust-project-struct-example-step-by-step-3ee
PREV
使用 Tailwind CSS 进行 Dev.to 克隆
NEXT
使用 Probot 创建您的第一个 GitHub 机器人