在面试时(尤其是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
模块系统,因为它提供更好的模块化支持。