在面试时(尤其是Node.js后端的面试),我们经常会遇到这样一个问题:
“介绍一下
require的模块加载机制”。
🙋♂️🙋♀️:
Node.js引入一个模块时,优先从缓存加载。- 如果缓存中不存在,且模块名为核心模块,则直接加载。
- 如果是路径形式的模块名,进一步分析标识符:
- 如果得到的是文件,则使用真实路径加载。
- 如果得到的是目录,则解析当前目录下的
package.json文件的main字段,获取入口文件。- 如果
package.json文件不存在或main字段为空,则使用index作为文件名查找。- 如果最后没有找到模块,则抛出
MODULE_NOT_FOUND错误。- 如果是非路径形式的模块名,即
自定义模块,则从当前目录下的node_modules目录开始逐级查找,直到根目录。- 如果最后没有找到模块,则抛出
MODULE_NOT_FOUND错误。
上述的回答虽然正确,但是还缺少一些细节。所以,我们有必要来详细了解一下Node.js中模块引入机制。
Node.js模块系统最初是基于CommonJS规范实现的,但从v12.x版本开始,原生支持了ES Module。下面,我们分别来了解两种规范的模块引入机制。
1. 基于CommonJS规范的模块系统
引入
使用require()方法引入模块。
在Node.js中每个文件都是一个独立的模块,并拥有自己的作用域。模块分为:
- 核心模块:
Node.js提供的模块,在编译源代码的过程中,被编译进了二进制文件。进程启动时,部分核心模块会直接加载到内存中。核心模块又分为C/C++模块和Javascript模块。 - 文件模块:用户编写的模块,运行时动态加载,加载速度慢于核心模块。
加载
模块加载,需要经历如下步骤:
- 以模块名为索引,优先从内存中加载。其中,核心模块从
NativeModule._cache对象中读取,文件模块则从Module._cache对象中读取。 - 模块名为核心模块,如
fs、path等,则直接加载并写入NativeModule._cache对象中。 - 模块名为路径形式,如
.、..、\开头的,文件分析时会转为真实路径,如果得到的是:- 文件
- 如果显示指定了扩展名,则使用真实路径加载。
- 否则,根据
.js、.json、.node的顺序补足扩展名,依次查找(可以通过指定文件的扩展名提高查找速度)。
- 目录
- 解析当前目录下的
package.json文件中的main字段,获取对应文件。 - 如果
package.json文件不存或main字段为空,则默认以index作为文件名,依次补足扩展名尝试加载。
- 解析当前目录下的
- 如果没有找到对应文件,则抛出
MODULE_NOT_FOUND错误。
- 文件
- 非路径形式模块名,则视为
自定义模块(第三方模块):- 查找当前目录下
node_modules目录,按照上述“将目录路径作为模块名”的方式查找。 - 逐级往上层的
node_modules目录查找,直到根目录下的node_modules目录。
- 查找当前目录下
- 如果通过设置
NODE_PATH环境变量添加了额外的模块查找路径,还会在NODE_PATH指定的路径中查找。 - 如果遍历所有模块路径都没有找到对应的模块,则抛出
MODULE_NOT_FOUND错误。
注意:
- 不同扩展名的文件处理方式不同:
.js:通过fs模块同步读取文件。在编译过程中,对文件内容进行头尾封装后,通过vm.runInContext()方法执行。.json: 通过fs模块同步读取文件,使用JSON.parse解析返回结果。.node:c/c++编写的模块,通过dlopen()方法加载文件后返回。在*nix平台中对应的是.so文件,在win平台中对应的是.ddl文件。- 在加载
Javascript核心模块时也需要经历编译执行的过程。- 用户模块在成功加载后,都会写入
Module._cache对象中,以提高模块的二次引入的加载速度。
js文件被包装后的样子如下:
// 字符串的形式,这里只是为了便于展示
(function (exports, require, module, filename, __dirname) {
var fs = require('fs')
exports.readCSVFile = function (filename) {
const rows = []
//TODO....
return rows
}
})
这也是为什么我们可以直接在js文件中使用require、exports、__dirname等变量的原因。
此外,借助Node.js命令行界面,可以查看当前目录下模块的查找路径:
2. 基于ECMAScript规范模块系统
引入
使用import语句导入模块。
要在Node.js中使用import语法,必须满足以下条件之一:
- 文件必须使用
.mjs作为扩展名 - 修改当前目录下的
package.json文件,添加{ type: module }
加载
- 静态导入
- 使用
import语句。 - 编译时解析。
- 运行时同步加载。
- 使用
- 动态导入
- 使用
import()方法。 - 运行时解析。
- 运行时异步加载。
- 通常用于实现按需加载或者懒加载。
- 使用
使用import()方法动态导入模块例子:
// 返回的是一个Promise对象
import('lodash')
.then(({ default: _ }) => {
console.log(_.random(1, 100))
})
它通常用于实现按需加载或者懒加载。
总结
Node.js支持CommonJs和ES6模块系统,如果混合使用(不建议),则需要遵循一些规则和限制。- 如果使用的是较高版本的
Node.js,推荐使用ES6模块系统,因为它提供更好的模块化支持。