清楚 node 模块查找策略可以帮我们更好熟悉工程化,毕竟模块化是工程化之基,本篇文章就带大家一起学习 node.js 模块查找策略
node.js 模块查找算法
node 官网中关于 node.js 模块查找算法的解释总结如下

翻译出来就是下面的意思,记住这张图,里面的函数稍后会作解释

这里有两个参数,其中 X
就是 require
的入参,Y
就是当前文件的路径,一个文件就是一个模块,因此我们也可以说 Y
就是当前模块的所在路径
可以看到这个算法有 7 个步骤,除了里面涉及到的几个函数,其余我们都可以理解
第一步:内置模块去查找
node 内置模块有很多,比如 fs,http, path 等,其余的模块就是一些需要 install 的依赖,比如 lodash
,因此 node 会优先识别内置模块,要是找不到就往后续步骤执行

第二步:X
为 /
开头,则识别为绝对路径去查找
这里我以 windows
举例,windows
的绝对路径和 mac/linux
不同,windows
用的是反斜杠 \
,而反斜杠 \
又恰好是 js
的转义字符,所以这里需要写成 两个 \
才行

第三步:X
为 ./
、/
或 ../
开头,则识别为相对路径去查找
如果 X
等于 '.',或者 X
以 './'、'/' 或 '../' 开头,那么就会依次执行下面三个步骤
LOAD_AS_FILE(Y + X)
LOAD_AS_DIRECTORY(Y + X)
- 抛出 "not found" 错误
先看 LOAD_AS_FILE(Y + X)
的内容
若不考虑
LOAD_AS_FILE(Y + X)
的第二小步的继续深入,其实 LOAD_AS_FILE(Y + X)
的意思就是
- 直接加载对应后缀的文件,比如
require('./test.js')
- 自动添加 .js 后缀,比如当前目录下有 test.js ,我们还可以直接写
require('./test')
- 直接加载 .json 文件,比如
require('./config.json')
- 直接加载 .node 文件,比如
require('./test.node')
,一般 node 都是二进制无法阅读
现在我们进入 LOAD_AS_FILE(Y + X)
的第二小步,也就是假设 require('./test')
时当前目录存在 test.js 时,node 应该如何加载 这个 test.js
md
a. 找到距离 X 最近的包作用域 SCOPE。
b. 如果没有找到作用域
1. MAYBE_DETECT_AND_LOAD(X.js)
c. 如果 SCOPE/package.json 包含 "type" 字段,
1. 如果 "type" 字段是 "module",将 X.js 作为 ECMAScript 模块加载。停止。
2. 如果 "type" 字段是 "commonjs",将 X.js 作为 CommonJS 模块加载。停止。
d. MAYBE_DETECT_AND_LOAD(X.js)
这里解释什么是 包作用域 SCOPE
,所谓包作用域 SCOPE
就是包含 package.json
的目录,比如 vue
源码里面的 各种 package
,其中有 compiler-sfc
,那么这个目录下的范围就是一个 包作用域 SCOPE
也就是说一个包的 package.json
里面的 type
字段直接影响了这个包内所有的 .js
文件的加载方式,比如 type: module
就意味着这个包只能使用 ES
模块加载方式,与之对应的 type: commonjs
就只能使用 commonjs
模块加载方式
如果没有找到 package.json
,或者有 package.json
但是没有 type
字段,那么就会调用 MAYBE_DETECT_AND_LOAD(X.js)
,这个函数作用是自动检测模块语法
🙋♀️🌰,我在 package.json
设置了 type: module
json
{
"name": "demo",
"type": "module"
}
那么我在 app.js 想要导入 test.js ,那么就不能用 require()
,只能 import
js
import testFunc from './test.js';
要是 没有 package.json
或者 package.json
没有设置 type
字段,MAYBE_DETECT_AND_LOAD(X.js)
是如何自动检测模块语法的呢
其实很简单,我们也可以想到,import/export
是 esm
语法,module.exports/require
是 commonjs
语法,它会扫描这个关键字来自动判断
第一个函数没有结果就执行 第二个函数 LOAD_AS_DIRECTORY
,也就是说文件找不到时,那就当做文件夹去找

总共就两个步骤,先深入第一步
这个函数处理的就是文件夹的情况,先看 若 X 是文件夹,那其下的 package.json
的 main
字段,也就是包的入口文件,一般都会设置成 main.js
,若字段为空,执行 第二小步 的 LOAD_INDEX(X)
,这个函数下面解释
第三小步,加载 X + main
字段的值
第四小步,main
文件不存在,尝试以 main
作为 目录,也就是执行上面的 LOAD_AS_FILE
第五小步,若 main
字段的文件不存在,尝试加载 x/index.js
,可以理解为 main
的默认值就是 index.js
第二步就是 LOAD_INDEX(X)
,这个函数的步骤,就是针对 index
换后缀,顺序依次为 js
,json
,node
第四步:X
为 #
开头
require()
的参数若 以 #
开头,就是表示这个包是私有包,外部无法访问,这个还比较少见
比如我在当前目录下 app.js
文件中 require('#utils')
那么意味着当前目录的 package.json
配置就需要在 imports
字段中声明这个私有包的位置
json
{
"name": "demo",
"imports": {
"#utils": "./src/utils.js"
}
}
我若是在 当前目录下新建一个 包(包含package.json
),然后里面去通过 #
引用外层的私有包 utils
,那么就会报错,不过依旧可以去用 相对路径 引用,所以这么看这个私有好像也没啥用
回到第四步,若 参数 以 #
开头,那么就会调用函数 LOAD_PACKAGE_IMPORTS(X, dirname(Y))

DIR 是入参 dirname(Y)
,前面提到过 Y 是当前 require()
所在文件的路径,那么 dirname(Y)
就是当前文件所在的文件夹的路径,也就是去掉了当前文件,所以 DIR 就是当前包
md
Y (当前文件路径): C:\Users\22922\Desktop\module\app.js
dirname(Y) (目录部分): C:\Users\22922\Desktop\module
第二步,找最近的 SCOPE
指的是从当前 SCOPE
向外层找
第四步,若启用了 --experimental-require-module
,指的是命令行,比如 scripts
中我们可以设置 这个命令行
json
{
"scripts": {
"dev": "node --experimental-require-module app.js"
}
}
启用了这个 ,node
内部会设置 conditions
数组,启用后的作用就是可以让 commonjs
同步 require esm
语法导出的模块,require
本身就是同步的,这个启动配置可以让其支持 同步 esm
语法的导出
第五步其实就是将 X 也就是 # 开头的参数用 pathToFileURL
解析成 file://URL
格式,其实就是 #utils
映射成 相对路径,然后 内部添加 conditions
数组
第六步,将 file://URL
转换回文件系统路径,验证文件真实存在,以及后缀,成功加载并返回模块内容
第五步:LOAD_PACKAGE_SELF(X, dirname(Y))
同样,前两步从自身
SCOPE
往外层查找,其实就是找 package.json
第三步检查 package.json
的 exports
字段
json
{
"name": "my-package",
"exports": {
".": "./main.js",
"./utils": "./utils.js",
"./config": "./config.json"
}
}
第四步检查包名匹配,比如这里 require('my-package')
就会加载 main.js
第五步 PACKAGE_EXPORTS_RESOLVE
解析路径
require('my-package')
就是 "." + "" 找到 ./main.js
require('my-pckage/utils')
就是 "." + "/utils" 找到 ./utils.js
第六步 就是将 上一步 返回的 match (url)
转换回文件系统路径,然后检查文件真实存在,依据后缀名返回实际的模块内容
第六步:LOAD_NODE_MODULES(X, dirname(Y))

第一步生成 node_modules
目录路径(可能包含多种情况),比如 当前 scope
为 /home/user/project/src/components
那么 DIRS 数组可能为
js
DIRS = [
'/home/user/project/src/components/node_modules',
'/home/user/project/src/node_modules',
'/home/user/project/node_modules',
'/home/user/node_modules',
'/home/node_modules',
'/node_modules',
...全局目录
]
第二步,尝试在每个 DIR 中尝试三种加载方式
第一种使用包的 exports
字段加载,这就要看 package.json
的 exports
字段配置了
第二种将当前 require
入参作为文件加载
第三种将当前 require
入参作为目录加载
第七步:return 'not found'
简洁版
看完肯定很绕,而且一些内容是我们平时开发中很少接触的,那就总结一下 node
模块查找算法的简洁版
-
内置模块(比如 fs,path,file)
-
路径(先绝对后相对)
-
尝试作为文件查找(
LOAD_AS_FILE
)a. ./config
b. ./config.js(根据最近包(从自身往外层)的
package.json
的type
字段)c. ./config.json
d. ./config.node
-
尝试作为文件夹查找(
LOAD_AS_DIRECTORY
,看package.json
的main
入口文件,要是没注册就默认index.js
) -
#
开头的私有包(需要package.json
的imports
去注册) -
包内部引用自己的子模块(
require('my-package/utils')
,需要exports
字段注册) -
查找依赖
-
找不到报错
这么看第三步和第四步才是核心,原来从内置模块查找第三方库的依赖需要调尽这个算法全部过程
文章中若出现错误内容还请各位大佬见谅并指正。如果有任何问题或建议,欢迎指出,另外,有不懂之处欢迎在评论区留言。如果觉得文章对你的学习有所帮助,还请 关注、点赞、收藏 一键三连,感谢支持!欢迎关注我的公众号:
Dolphin_Fung