【原创】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.js
的bar
变量此时尚未初始化,所以b.js
访问bar
时得到undefined
。
3. ES Modules 的加载方式
ES Modules 的加载方式主要有三种:
- 在 HTML 中通过
<script type="module">
直接加载 - 在 Node.js 中使用 ES 模块
- 使用
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 中,加载顺序遵循以下规则:
- 解析
import
依赖:当import
语句出现时,浏览器或 Node.js 不会立即执行代码,而是先解析所有模块依赖。 - 并行下载所有依赖模块(异步):
- 浏览器或 Node.js 会并行加载所有
import
依赖的模块,提高效率。 - 但它们的执行顺序仍然遵循源码顺序。
- 浏览器或 Node.js 会并行加载所有
- 模块执行顺序:
- 先执行无依赖的模块(最底层模块)。
- 再执行有依赖的模块(逐层向上执行)。
- 最后执行主文件。
示例:加载顺序
// 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 |
---|---|
加载方式 | import 和 import() |
加载行为 | 默认异步,不阻塞主线程 |
解析方式 | 静态解析,不能动态 import 变量 |
代码执行 | 仅执行一次,全局唯一 |
适用场景 | 浏览器、Node.js |
ES Modules 是现代 JavaScript 的推荐模块系统,提供了更好的性能、模块化管理和可维护性。如果你在构建现代前端或后端应用,建议使用 ES Modules 取代 CommonJS。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章