你真的了解ESM吗?

简介: 模块兼容性 推荐阅读指数⭐️⭐️⭐️ESM是个啥 推荐阅读指数⭐️⭐️⭐️⭐️在浏览器中使用ESM 推荐阅读指数⭐️⭐️⭐️⭐️模块指定符 推荐阅读指数⭐️⭐️⭐️⭐️⭐️默认情况下,模块是defer的 推荐阅读指数⭐️⭐️⭐️⭐️⭐️动态导入 推荐阅读指数⭐️⭐️⭐️⭐️⭐️import.meta 推荐阅读指数⭐️⭐️⭐️⭐️⭐️优化建议 推荐阅读指数⭐️⭐️⭐️网站中使用ESM 推荐阅读指数⭐️⭐️⭐️


所有系统都有一种自毁趋势,往“熄灭”或者“圆寂”方向发展。这个趋势就叫“熵增”。为了维持系统,需要持续的输入能量,这种持续输入的能量我们就叫“负熵流”。 《向上生长》

大家好,我是柒八九

今天,我们继续Rust学习笔记的探索。我们来谈谈关于包、Crate和模块的相关知识点。

如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。

文章list

  1. Rust学习笔记之Rust环境配置和入门指南
  2. Rust学习笔记之基础概念
  3. Rust学习笔记之所有权
  4. Rust学习笔记之结构体
  5. Rust学习笔记之枚举和匹配模式

你能所学到的知识点

  1. Rust中包和 crate  推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
  2. 模块控制作用域与私有性  推荐阅读指数 ⭐️⭐️⭐️⭐️
  3. 路径用于引用模块树中的项 推荐阅读指数 ⭐️⭐️⭐️⭐️
  4. use名称引入作用域 推荐阅读指数 ⭐️⭐️⭐️⭐️
  5. 将模块分割进不同文件  推荐阅读指数 ⭐️⭐️⭐️

好了,天不早了,干点正事哇。


伴随着项目的增长,你可以通过将代码分解为多个模块和多个文件来组织代码。一个包可以包含多个二进制 crate 项和一个可选的 crate。伴随着包的增长,你可以将包中的部分代码提取出来,做成独立的 crate,这些 crate 则作为外部依赖项

Rust{模块系统|the module system},包括:

  • 包(Packages): Cargo 的一个功能,它允许你构建、测试和分享 crate
  • Crates :一个模块的树形结构,它形成了库或二进制项目。
  • 模块(Modules)和 use: 允许你控制作用域和路径的私有性。
  • 路径(path):一个命名例如结构体、函数或模块等项的方式

包和 crate

  • 包(package) 是**提供一系列功能的一个或者多个 crate。**一个包会包含有一个 Cargo.toml 文件,阐述如何去构建这些 crate
  • crate 是一个二进制项或者库crate root 是一个源文件Rust 编译器以它为起始点,并构成你的 crate 的根模块。

包中所包含的内容由几条规则来确立。

  1. 一个包中至多只能包含一个{库 crate|library crate}
  2. 包中可以包含任意多{二进制 crate|binary crate}
  3. 包中至少包含一个 crate,无论是库的还是二进制的。

输入命令 cargo new

$ cargo new my-project
     Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs
复制代码

当我们输入了这条命令,Cargo 会给我们的创建一个 Cargo.toml 文件。查看 Cargo.toml 的内容,会发现并没有提到 src/main.rs,因为 Cargo 遵循的一个约定:

  • src/main.rs 就是一个与包同名{二进制 crate|binary crate}crate 根。
  • 同样的,Cargo 知道如果包目录中包含 src/lib.rs,则包带有与其同名的{库 crate|library crate},且 src/lib.rscrate 根。

crate 根文件将由 Cargo 传递给 rustc 来实际构建库或者二进制项目。

如果一个包同时含有src/main.rssrc/lib.rs,则它有两个 crate一个库和一个二进制项,且名字都与包相同

通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制crate:每个 src/bin 下的文件都会被编译成一个独立的{二进制 crate|binary crate}

一个 crate 会将一个作用域内的相关功能分组到一起,使得该功能可以很方便地在多个项目之间共享


定义模块来控制作用域与私有性

模块 让我们可以将一个 crate 中的代码进行分组,以提高可读性与重用性。模块还可以控制项的 私有性

  • 是可以被外部代码使用的(public
  • 还是作为一个内部实现的内容,不能被外部代码使用(private)。

通过执行 cargo new --lib restaurant,来创建一个新的名为 restaurant的库。在 restaurant/src/lib.rs 中,来定义一些模块和函数。

fn main() {
  mod front_of_house {
      mod hosting {
          fn add_to_waitlist() {}
          fn seat_at_table() {}
      }
      mod serving {
          fn take_order() {}
          fn server_order() {}
          fn take_payment() {}
      }
  }
}
复制代码

关键字 mod 定义一个模块,指定模块的名字,并用大括号包围模块的主体。我们可以在模块中包含其他模块,就像本示例中的 hostingserving 模块。模块中也可以包含其他项,比如结构体、枚举、常量、trait

通过使用模块,我们可以把相关的定义组织起来,并通过模块命名来解释为什么它们之间有相关性。使用这部分代码的开发者可以更方便的循着这种分组找到自己需要的定义,而不需要通览所有。编写这部分代码的开发者通过分组知道该把新功能放在哪里以便继续让程序保持组织性。

之前我们提到,src/main.rssrc/lib.rs 被称为 crate 根。如此称呼的原因是,这两个文件中任意一个的内容会构成名为 crate 的模块,且该模块位于 crate 的被称为 {模块树 的模块结构的根部|at the root of the crate’s module structure}

上面的代码所对应的模块树如下所示。

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment
复制代码

这个树展示了模块间是如何相互嵌套的。这个树还展示了一些模块互为兄弟 ,即它们被定义在同一模块内。


路径用于引用模块树中的项

Rust 使用路径的方式在模块树中找到一个的位置,就像在文件系统使用路径一样。如果我们想要调用一个函数,我们需要知道它的路径。

路径有两种形式:

  1. {绝对路径|absolute path}crate 根部开始,以 crate 名或者字面量 crate 开头。
  2. {相对路径|relative path}当前模块开始,以 selfsuper 或当前模块的标识符开头。

绝对路径相对路径都后跟一个或多个由双冒号(::)分割的标识符。

crate 根部定义了一个新函数 eat_at_restaurant,并在其中展示调用 add_to_waitlist 函数的两种方法。eat_at_restaurant 函数是我们 crate 库的一个公共 API,所以我们使用 pub 关键字来标记它

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}
pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();
    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}
复制代码

第一种方式,我们在 eat_at_restaurant 中调用 add_to_waitlist 函数,使用的是绝对路径add_to_waitlist 函数与 eat_at_restaurant 被定义在同一 crate 中,这意味着我们可以使用 crate 关键字为起始的绝对路径

crate 后面,我们持续地嵌入模块,直到我们找到 add_to_waitlist。你可以想象出一个相同结构的文件系统,我们通过指定路径 /front_of_house/hosting/add_to_waitlist 来执行 add_to_waitlist 程序。我们使用 cratecrate 根部开始就类似于在 shell 中使用 / 从文件系统根开始。

第二种方式,我们在 eat_at_restaurant 中调用 add_to_waitlist,使用的是相对路径。这个路径以 front_of_house 为起始,这个模块在模块树中,与 eat_at_restaurant 定义在同一层级。与之等价的文件系统路径就是 front_of_house/hosting/add_to_waitlist。以名称为起始,意味着该路径是相对路径。

模块不仅对于你组织代码很有用。他们还定义了 Rust{私有性边界|privacy boundary}:这条界线不允许外部代码了解、调用和依赖被封装的实现细节。所以,如果你希望创建一个私有函数或结构体,你可以将其放入模块

Rust默认所有项(函数、方法、结构体、枚举、模块和常量)都是私有的。

  • 父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用他们父模块中的项
    这是因为子模块封装并隐藏了他们的实现详情,但是子模块可以看到他们定义的上下文

使用 pub 关键字暴露路径

想让父模块中的 eat_at_restaurant 函数可以访问子模块中的 add_to_waitlist 函数,因此我们使用 pub 关键字来标记 hosting 模块

mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist() {}
    }
}
pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();
    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}
复制代码

cargo build的时候,代码编译仍然有错误。

modhosting 前添加了 pub 关键字,使其变成公有的。伴随着这种变化,如果我们可以访问 front_of_house,那我们也可以访问 hosting。但是 hosting{内容|contents}仍然是私有的;

这表明使模块公有并不使其内容也是公有的。模块上的 pub 关键字只允许其父模块引用它

继续将 pub 关键字放置在 add_to_waitlist 函数的定义之前,使其变成公有。

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}
pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();
    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}
复制代码

使用 super 起始的相对路径

还可以使用 super 开头来构建从父模块开始的相对路径。这么做类似于文件系统中以 .. 开头的语法。

如下模拟了厨师更正了一个错误订单,并亲自将其提供给客户的情况。fix_incorrect_order 函数通过指定的 super 起始的 serve_order 路径,来调用 serve_order 函数:

fn serve_order() {}
mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::serve_order();
    }
    fn cook_order() {}
}
复制代码

fix_incorrect_order 函数在 back_of_house 模块中,所以我们可以使用 super 进入 back_of_house 父模块,也就是本例中的 crate 根。在这里,我们可以找到 serve_order。成功!


创建公有的结构体和枚举

还可以使用 pub 来设计公有的结构体枚举,不过有一些额外的细节需要注意。如果我们在一个结构体定义的前面使用了 pub ,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的。我们可以根据情况决定每个字段是否公有

定义了一个公有结构体 back_of_house:Breakfast,其中有一个公有字段 toast 和私有字段 seasonal_fruit

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }
    impl Breakfast {
        // 定义关联函数
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("桃子"),
            }
        }
    }
}
pub fn eat_at_restaurant() {
    let mut meal = back_of_house::Breakfast::summer("Rye");
    meal.toast = String::from("Wheat");
    println!("我喜欢吃{} ", meal.toast);
}
复制代码

因为 back_of_house::Breakfast 结构体的 toast 字段是公有的,所以我们可以在 eat_at_restaurant 中使用点号来随意的读写 toast 字段。

因为 back_of_house::Breakfast具有私有字段,所以这个结构体需要提供一个公共的关联函数来构造 Breakfast 的实例。如果 Breakfast 没有这样的函数,我们将无法在 eat_at_restaurant 中创建 Breakfast 实例,因为我们不能在 eat_at_restaurant 中设置私有字段 seasonal_fruit 的值。

如果我们将枚举设为公有,则它的所有成员都将变为公有。我们只需要在 enum 关键字前面加上 pub

mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}
pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}
复制代码

使用 use 关键字将名称引入作用域

我们可以使用 use 关键字路径一次性引入作用域,然后调用该路径中的项,就如同它们是本地项一样。

crate::front_of_house::hosting 模块引入了 eat_at_restaurant 函数的作用域,而我们只需要指定 hosting::add_to_waitlist 即可在 eat_at_restaurant 中调用 add_to_waitlist 函数。

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}
fn main() {}
复制代码

在作用域中增加 use 和路径类似于在文件系统中创建软连接{符号连接|symbolic link})。通过在 crate 根增加 use crate::front_of_house::hosting,现在 hosting 在作用域中就是有效的名称了,如同 hosting 模块被定义于 crate 根一样。通过 use 引入作用域的路径也会检查私有性,同其它路径一样。

还可以使用 use相对路径来将一个引入作用域。

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}
use front_of_house::hosting;
pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}
fn main() {}
复制代码

创建惯用的 use 路径

要想使用 use 将函数的父模块引入作用域,我们必须在调用函数时指定父模块,这样可以清晰地表明函数不是在本地定义的,同时使完整路径的重复度最小化。

另一方面,使用 use 引入结构体、枚举和其他项时,习惯是指定它们的完整路径

use std::collections::HashMap;
fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}
复制代码

这个习惯用法有一个例外,那就是我们想使用 use 语句将两个具有相同名称的项带入作用域,因为 Rust 不允许这样做。

fn main() {
  use std::fmt;
  use std::io;
  fn function1() -> fmt::Result {
      // --snip--
      Ok(())
  }
  fn function2() -> io::Result<()> {
      // --snip--
      Ok(())
  }
}
复制代码

使用父模块可以区分这两个 Result 类型。如果我们是指定 use std::fmt::Resultuse std::io::Result,我们将在同一作用域拥有了两个 Result 类型,当我们使用 Result 时,Rust 则不知道我们要用的是哪个。


使用 as 关键字提供新的名称

使用 use 将两个同名类型引入同一作用域这个问题还有另一个解决办法:在这个类型的路径后面,我们使用 as 指定一个新的本地名称或者别名

fn main() {
  use std::fmt::Result;
  use std::io::Result as IoResult;
  fn function1() -> Result {
      // --snip--
      Ok(())
  }
  fn function2() -> IoResult<()> {
      // --snip--
      Ok(())
  }
}
复制代码

在第二个 use 语句中,我们选择 IoResult 作为 std::io::Result 的新名称,它与从 std::fmt 引入作用域的 Result 并不冲突。


使用 pub use 重导出名称

当使用 use 关键字将名称导入作用域时,在新作用域中可用的名称是私有的。如果为了让调用你编写的代码的代码能够像在自己的作用域内引用这些类型,可以结合 pubuse。这个技术被称为 {重导出|re-exporting},因为这样做将引入作用域并同时使其可供其他代码引入自己的作用域。

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}
fn main() {}
复制代码

通过 pub use,现在可以通过新路径 hosting::add_to_waitlist 来调用 add_to_waitlist 函数。如果没有指定 pub useeat_at_restaurant 函数可以在其作用域中调用 hosting::add_to_waitlist,但外部代码则不允许使用这个新路径


使用外部包

假设项目使用了一个外部包rand,来生成随机数。为了在项目中使用 rand,在 Cargo.toml 中加入了如下行:

文件名: Cargo.toml

[dependencies]
rand = "0.8.3"
复制代码

Cargo.toml 中加入 rand 依赖告诉了 Cargo 要从 crates.io 下载 rand和其依赖,并使其可在项目代码中使用。

接着,为了将 rand 定义引入项目包的作用域,我们加入一行 use 起始的包名,它以 rand 包名开头并列出了需要引入作用域的

use rand::Rng;
fn main() {
    let secret_number = rand::thread_rng()
                        .gen_range(1..101);
}
复制代码

crates.io 上有很多 Rust 社区成员发布的包,将其引入你自己的项目都需要一道相同的步骤:在 Cargo.toml 列出它们并通过 use 将其中定义的引入项目包的作用域中。

标准库(std)对于你的包来说也是外部 crate。因为标准库随 Rust 语言一同分发,无需修改 Cargo.toml 来引入 std,不过需要通过 use 将标准库中定义的引入项目包的作用域中来引用它们,比如我们使用的 HashMap

fn main() {
    use std::collections::HashMap;
}
复制代码

嵌套路径来消除大量的 use 行

当需要引入很多定义于相同包或相同模块的时,为每一项单独列出一行会占用源码很大的空间。

fn main() {
  use std::cmp::Ordering;
  use std::io;
}
复制代码

我们可以使用嵌套路径将相同的在一行中引入作用域。这么做需要指定路径的相同部分,接着是两个冒号,接着是大括号中的各自不同的路径部分

fn main() {
  use std::{cmp::Ordering, io};
// ---snip---
}
复制代码

通过 glob 运算符将所有的公有定义引入作用域

如果希望将一个路径下 所有公有项引入作用域,可以指定路径后跟 *

fn main() {
  use std::collections::*;
}
复制代码

这个 use 语句将 std::collections 中定义的所有公有项引入当前作用域。


将模块分割进不同文件

当模块变得更大时,你可能想要将它们的定义移动到单独的文件中,从而使代码更容易阅读。

front_of_house 模块移动到属于它自己的文件 src/front_of_house.rs中,通过改变 crate 根文件。在这个例子中,crate 根文件是 src/lib.rs,这也同样适用于以 src/main.rscrate 根文件的二进制 crate 项。

文件名: src/lib.rs

mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}
复制代码

文件名: src/front_of_house.rs

pub mod hosting {
    pub fn add_to_waitlist() {}
}
复制代码

mod front_of_house使用分号,而不是代码块,这将告诉 Rust 在另一个与模块同名的文件中加载模块的内容。


后记

分享是一种态度

参考资料:

  1. 《Rust权威指南》
  2. Rust语言圣经(Rust Course)

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。


相关文章
|
2月前
|
缓存 JavaScript 前端开发
JavaScript模块化开发:ES6模块与CommonJs的对比与应用
JavaScript模块化开发:ES6模块与CommonJs的对比与应用
32 2
|
6月前
|
JavaScript
JS模块化规范之ES6及UMD
JS模块化规范之ES6及UMD
86 3
|
11月前
|
JavaScript
browserify
browserify
46 1
|
JavaScript 前端开发 Ruby
打包出来的 common.js/umd.js 是啥?
打包出来的 common.js/umd.js 是啥?
276 0
|
设计模式 前端开发 JavaScript
CJS,AMD,UMD和ESM区别
说到 CJS, AMD, UMD 和 ESM 四者的区别,不得不提一下模块这个概念。 CJS CJS(CommonJs) 适用于后端 Node。 Node 与 Javascript 最开始是没有模块这
345 0
|
Web App开发 前端开发 JavaScript
UMD 被淘汰了吗?不考虑的 UMD 的库如何在纯 UMD 前端项目中运行?
UMD 被淘汰了吗?不考虑的 UMD 的库如何在纯 UMD 前端项目中运行?
231 0
|
缓存 Rust 前端开发
esbuild + swc 能有多快?
前端工具层出不穷,之前有常用的打包工具webpack,现在有了速度更快的vite。 vite的开发模式是基于esBuild编译的,打包又是基于rollup,启动项目是很快的。
796 0
esbuild + swc 能有多快?
|
JSON JavaScript 前端开发
终于搞懂了 ESM 和 CJS 互相转换
终于搞懂了 ESM 和 CJS 互相转换
441 0
|
监控 JavaScript
babel编译js文件
babel编译js文件
183 0
|
JavaScript 前端开发 API
Vue3组件库打包指南,一次生成esm、esm-bundle、commonjs、umd四种格式
本文为`Varlet`组件库源码主题阅读系列第二篇,读完本篇,你可以了解到如何将一个Vue3组件库打包成各种格式
640 0