【原创】Commonjs模块和ESM模块

参考:

http://nodejs.cn/api/modules.html (nodejs官方教程)

常识性知识

nodejs的模块有2种类型:commonjs模块和es模块;

不可以 require() 具有 .mjs 扩展名的文件。 试图这样做会抛出错误。 .mjs 扩展名是保留给 ECMAScript 模块,无法通过 require() 加载。

 

模块封装器

在执行模块代码之前,Node.js 会使用一个如下的函数封装器将其封装

(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
});
  • 它保持了顶层的变量(用 var、 const 或 let 定义)作用在模块范围内,而不是全局对象。
  • 它有助于提供一些看似全局的但实际上是模块特定的变量,例如:
    • 实现者可以用于从模块中导出值的 module 和 exports 对象。
    • 包含模块绝对文件名和目录路径的快捷变量 __filename 和 __dirname 。

  

模块加载的顺序

没有路径符号前缀时

当没有以 '/'、 './' 或 '../' 开头来表示文件时,这个模块必须是一个核心模块或加载自 node_modules 目录

加载顺序为

复制代码
//1,核心模块

//2,node_modules目录

先找当前文件夹下的node_modules文件夹下的module-name文件夹下的package.json 文件指定的main字段文件路径。
## 如果第一种情况没有找到
找当前文件夹下的node_modules文件夹下的module.js 文件
## 如果第二种情况没有找到
找当前文件夹下的node_modules文件夹下的module文件夹下的index.js 文件
## 如果第三种情况没有找到
找的上一级node_modules文件夹,查找顺序与上面一样。

//3,全局目录加载

如果 NODE_PATH 环境变量被设为一个以冒号分割的绝对路径列表,则当在其他地方找不到模块时 Node.js 会搜索这些路径。 此外,Node.js 还会搜索以下的全局目录列表: 1: $HOME/.node_modules 2: $HOME/.node_libraries 3: $PREFIX/lib/node 其中 $HOME 是用户的主目录, $PREFIX 是 Node.js 里配置的 node_prefix。 强烈建议将所有的依赖放在本地的 node_modules 目录。 这样将会更快地加载,且更可靠。
复制代码

有'/'、 './' 或 '../' 前缀时

以 '/' 为前缀的模块是文件的绝对路径。 例如, require('/home/marco/foo.js') 会加载 /home/marco/foo.js 文件。
以 './' 为前缀的模块是相对于调用 require() 的文件的。 必须在同一目录下
以 '../' 为前缀的模块是相对于调用 require() 的文件的。 必须在上一目录下

加载顺序为

文件
module-name.js
module-name.node

目录
module-name/package.json的main属性
module-name/index.js
module-name/index.node

如果给定的路径不存在,则 require() 会抛出一个 code 属性为 'MODULE_NOT_FOUND' 的 Error。

 

模块分类

核心模块

Node.js 有些模块会被编译成二进制。 这些模块别的地方有更详细的描述。

核心模块定义在 Node.js 源代码的 lib/ 目录下。

require() 总是会优先加载核心模块。 例如, require('http') 始终返回内置的 HTTP 模块,即使有同名文件。

 

文件模块

其实模块都是文件;

按确切的文件名没有找到模块,则 Node.js 会尝试带上 .js、 .json 或 .node 拓展名再加载。

 

目录作为模块

如果存在同名文件,会优先使用同名文件;

require('./dir-module') 传入目录名,解析顺序是

//在该目录下查找package.json文件的main属性对应的模块
package.json文件下的main属性

//在该目录下查找index.js文件
./some-library/index.js

//在目录下查找index.node文件
./some-library/index.node

 

node_modules 目录下的模块

如果传递给 require() 的模块标识符不是一个核心模块,也没有以 '/' 、 '../' 或 './' 开头,则 Node.js 会从当前模块的父目录开始,尝试从它的 /node_modules 目录里加载模块。

如果还是没有找到,则移动到再上一层父目录,直到文件系统的根目录。

例子,如果在 '/home/ry/projects/foo.js' 文件里调用了 require('bar.js'),则 Node.js 会按以下顺序查找:

  • /home/ry/projects/node_modules/bar.js
  • /home/ry/node_modules/bar.js
  • /home/node_modules/bar.js
  • /node_modules/bar.js

这使得程序本地化它们的依赖,避免它们产生冲突。

通过在模块名后包含一个路径后缀,可以请求特定的文件或分布式的子模块。 例如, require('example-module/path/to/file') 会把 path/to/file 解析成相对于 example-module 的位置。 后缀路径同样遵循模块的解析语法。

 

循环加载

module首次加载,就会生成副本,循环使用时会返回 module 的 exports 对象的 未完成的副本

a.js

console.log('a 开始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 结束');

b.js

console.log('b 开始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 结束');

main.js

console.log('main 开始');
const a = require('./a.js');
const b = require('./b.js');
console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);

当 main.js 加载 a.js 时, a.js 又加载 b.js。 此时, b.js 会尝试去加载 a.js。 为了防止无限的循环,会返回一个 a.js 的 exports 对象的 未完成的副本 给 b.js 模块。 然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。

当 main.js 加载这两个模块时,它们都已经完成加载。 因此,该程序的输出会是:

复制代码
$ node main.js
main 开始
a 开始
b 开始
在 b 中,a.done = false
b 结束
在 a 中,b.done = true
a 结束
在 main 中,a.done=true,b.done=true
复制代码

 

ES模块

ES Modules(ESM)加载特性、方式及加载顺序解析

1. ES Modules(ESM)概述

ES Modules(简称 ESM)是 JavaScript 的官方模块系统,它是 ECMAScript 2015(ES6)引入的,旨在解决 JavaScript 代码的模块化管理问题。相比 CommonJS,ESM 具有静态分析异步加载顶层 await 等特性,使其更适合现代 JavaScript 应用开发。


2. ESM 的加载特性

ESM 的加载方式与 CommonJS 有明显不同,主要体现在以下几个方面:

(1)默认异步加载

ESM 在浏览器和 Node.js 中都是异步加载的,不会阻塞主线程。这对于 Web 前端开发尤其重要,可以提高性能。

(2)静态解析

在 ES Modules 中,模块依赖关系在编译阶段就已经确定,这意味着:

  • 不能使用动态导入变量import someVariable 不是合法语法)。
  • import 语句必须位于顶层,不能在条件语句或函数内部。

示例:非法的动态导入

if (true) {
    import { add } from './math.js'; // ❌ 错误!import 必须在顶层
}

(3)模块是严格模式(Strict Mode)

ESM 默认使用严格模式,即便没有显式使用 "use strict",这有助于避免潜在的错误,例如:

  • 变量必须声明后使用
  • 禁止 this 绑定到 window(在非类方法中)

(4)模块代码仅加载一次,且全局唯一

当一个模块被多个文件导入时,它只会被执行一次,后续的 import 语句只会获取到模块的已解析结果,不会重新执行模块代码。

示例:ESM 只加载一次

// module.js
console.log("Module is loaded!");
export const value = 42;

// main1.js
import { value } from "./module.js";
console.log("Main1: ", value);

// main2.js
import { value } from "./module.js";
console.log("Main2: ", value);

// 运行 main1.js 和 main2.js

输出(只会打印一次 "Module is loaded!")

Module is loaded!
Main1: 42
Main2: 42

(5)ESM 允许循环依赖

在 ESM 中,模块可以相互依赖,ESM 不会阻塞,而是返回一个未完成的模块对象

示例:ESM 循环依赖

// a.js
import { foo } from './b.js';
console.log("A: ", foo);
export const bar = "Hello from A";

// b.js
import { bar } from './a.js';
console.log("B: ", bar);
export const foo = "Hello from B";

// main.js
import './a.js';
import './b.js';

输出

B: undefined
A: Hello from B

b.js 先加载 a.js,但 a.jsbar 变量此时尚未初始化,所以 b.js 访问 bar 时得到 undefined


3. ES Modules 的加载方式

ES Modules 的加载方式主要有三种:

  1. 在 HTML 中通过 <script type="module"> 直接加载
  2. 在 Node.js 中使用 ES 模块
  3. 使用 import() 动态加载

(1)HTML 中 <script type="module"> 直接加载

在浏览器环境中,ES Modules 可以通过 <script type="module"> 方式加载:

<!DOCTYPE html>
<html lang="en">
<head>
    <script type="module">
        import { greet } from "./utils.js";
        console.log(greet("World"));
    </script>
</head>
<body>
</body>
</html>

特点

  • type="module" 告诉浏览器以 ESM 方式解析。
  • 模块中的 import 语句不会阻塞 HTML 解析,import 加载的模块是异步执行的

(2)Node.js 中使用 ES 模块

Node.js 原生支持 ES Modules,但需要满足以下条件之一:

  • package.json 中指定 "type": "module"
  • .mjs 作为文件扩展名

示例

// package.json
{
  "type": "module"
}
// math.mjs
export function add(a, b) {
    return a + b;
}

// app.mjs
import { add } from './math.mjs';
console.log(add(3, 5)); // 8

特点

  • Node.js 加载 ES Modules 是异步的
  • 不能使用 require(),必须用 import

(3)使用 import() 动态加载

import() 允许在运行时动态导入模块,适用于按需加载场景,如路由懒加载。

示例:动态加载

function loadMathModule() {
    import('./math.js')
        .then(module => {
            console.log(module.add(2, 3)); // 5
        })
        .catch(error => console.error("Error loading module:", error));
}

loadMathModule();

特点

  • import() 返回一个 Promise,适用于异步加载模块。
  • 适用于按需加载条件导入懒加载场景。

4. ES Modules 的加载顺序

在 ES Modules 中,加载顺序遵循以下规则:

  1. 解析 import 依赖:当 import 语句出现时,浏览器或 Node.js 不会立即执行代码,而是先解析所有模块依赖
  2. 并行下载所有依赖模块(异步):
    • 浏览器或 Node.js 会并行加载所有 import 依赖的模块,提高效率。
    • 但它们的执行顺序仍然遵循源码顺序
  3. 模块执行顺序
    • 先执行无依赖的模块(最底层模块)。
    • 再执行有依赖的模块(逐层向上执行)。
    • 最后执行主文件

示例:加载顺序

// a.js
console.log("Loading A");
export const a = "A";

// b.js
import { a } from "./a.js";
console.log("Loading B", a);
export const b = "B";

// c.js
import { b } from "./b.js";
console.log("Loading C", b);

// main.js
import "./c.js";
console.log("Main Loaded");

执行顺序

Loading A
Loading B A
Loading C B
Main Loaded

先加载 a.js,然后 b.js,再 c.js,最后 main.js,遵循从底层模块到主模块的顺序。


5. 结论

特性ES Modules
加载方式 importimport()
加载行为 默认异步,不阻塞主线程
解析方式 静态解析,不能动态 import 变量
代码执行 仅执行一次,全局唯一
适用场景 浏览器、Node.js

ES Modules 是现代 JavaScript 的推荐模块系统,提供了更好的性能、模块化管理和可维护性。如果你在构建现代前端或后端应用,建议使用 ES Modules 取代 CommonJS。

 

posted @   指令跳动  阅读(116)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
点击右上角即可分享
微信分享提示