清楚 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