Node 模块加载类型:
- 加载 Node核心模块
require(X)
- 加载 工程师自己编写的本地模块
require(./X) // 相对路径
require(../X) // 相对路径
require(/X) // 系统根目录,不常用
require(./X.[extention])
require(../X.[extention])
require(/X.[extention])
- 加载 安装的第三方模块
require(X)
Node 模块加载顺序
以 require(X) 为例:
先了解几个文件查找过程:
LOAD_AS_FILE(X):
查找 X.js 文件
查找 X.json 文件
查找 X.node 文件
LOAD_AS_INDEX(X):
查找 X/index.js 文件
查找 X/index.json 文件
查找 X/index.node 文件
LOAD_AS_DIRECTOR(X):
-
X/package.json 文件存在
a. 查找 package.json 中的 "main" 属性
b. 如果 "main" 属性值为假值,即: "main" 属性不存在 / "main"属性值为空 / "main" 属性值没有找到对应的文件或文件夹, 执行 f
c. 如果 "main" 属性值为真值,let M = X + (package.json main field)
d. LOAD_AS_FILE(M), 未找到的话, 执行 e
e. LOAD_INDEX(M), 未找到的话, 执行 f
f. LOAD_INDEX(X), DEPRECATED; 测试时,node version 18, 还是会执行此查找流程,但是 会抛出 DeprecationWarning; 未找到的话,执行 g
g. THROW "not found"
X/package.json 文件不存在
a. 执行 LOAD_INDEX(X), 即 foo 文件夹下的 index 文件
LOAD_NODE_MODULES(X, START)
以 reuqire(X) 为例:
a. 如果 X 是以 '/'、'./'、'../' 开头,证明加载的是本地模块;
- X 没有后缀名,会按照以下顺序进行查找,找到后进行加载;
i. X 作为文件 查找,执行 LOAD_AS_FILE(X)
ii. X 作为文件夹路径 查找,执行 LOAD_AS_DIRECTOR(X)
iii. THROW "not found"
- X 有后缀名,会直接查找 X文件;
i. 找到进行加载;
ii. 找不到 THROW "not found";
b. 如果 X 不是 以 '/'、'./'、'../' 开头,那么证明要加载的 不是本地模块;
Node 首先会检索核心模块,找到核心模块X,加载 Node 核心模块X,STOP;
没找到核心模块X,那么会检索第三方库;即,node_modules 中的库;找到 第三方库X,加载库X;STOP;
Node 如何加载的第三方包:
首先,会在当前文件目录去寻找 node_modules 文件夹,从里边寻找 X 模块,如果没有找到会一次到上级目录中 继续寻找 node_modules 中的 X 模块;
依次类推,知道到找到系统根目录为止;
当嵌套的层次很深时,这个文件查找列表可能会变的很长。因此,在查找时做了如下优化:
- /node_modules 不会附加在一个以 /node_modules 结尾的文件夹后面。
demo:
/home/foo/bar/node_modules/node_modules/xx.js
如上的 /node_modules/node_modules 的这种情况是不会出现的;
- 如果调用 require() 方法的文件已经在 node_modules 的文件链路层级中了,那么最顶层的 node_modules 文件夹将被视为搜索的根目录;
demo:
如果在文件 /home/projects/foo/node_modules/bar/node_modules/baz/a.js 中,调用了 require('b.js'); 则 Node 搜索的过程为:
/home/projects/foo/node_modules/bar/node_modules/baz/node_modules/b.js
/home/projects/foo/node_modules/bar/node_modules/b.js
/home/projects/foo/node_modules/b.js
至此就结束了,不会继续查找到 根目录;
require() 是如何找到确切要加载的文件名
require(X) from module at path Y
Y 代表当前目录(绝对路径)
//
A:LOAD_AS_FILE(X) - 加载文件
foo.js 存在, load foo.js
foo.js 不存在, load foo.json
foo.json 不存在, load foo.node
foo.node 不存在,THROW "not found"
//
B. LOAD_INDEX(X) - 加载入口文件
load foo/index.js
foo/index.js 不存在, load foo/index.json
foo/index.json 不存在, load foo/index.node
foo/index.node 不存在, THROW "not found"
//
C. LOAD_AS_DIRECTORY(X) - 加载文件夹
-
foo/package.json 文件存在
a. 查找 package.json 中的 "main" 属性
b. 如果 "main" 属性值为假值,即: "main" 属性不存在 / "main"属性值为空 / "main" 属性值没有找到对应的文件或文件夹, 执行 LOAD_AS_INDEX(X) 流程
c. 如果 "main" 属性值为真值,let M = X + (package.json main field)
d. LOAD_AS_FILE(M), 未找到的话, 执行 e
e. LOAD_INDEX(M), 未找到的话, 执行 f
f. LOAD_INDEX(X), DEPRECATED; 测试时,node version 18, 还是会执行此查找流程,但是 会抛出 DeprecationWarning; 未找到的话,执行 g
g. THROW "not found"
foo/package.json 文件不存在
a. 执行 LOAD_INDEX(X), 即 foo 文件夹下的 index 文件
//
D. LOAD_NODE_MODULES(X, STRAT) - 加载 node模块
// 注意这里的 START 是当前其实路径
let DIRS = NODE_MODULES_PATHS(START)
for each DIR in DIRS:
a. LOAD_PACKAGE_EXPORTS(X, DIR)
b. LOAD_AS_FILE(DIR/X)
c. LOAD_AS_DIRECTORY(DIR/X)
//
E. NODE_MODULES_PATHS(START) - 列出所有可能的 node_modules 路径
let PARTS = path split(START)
let I = count of PARTS - 1
let DIRS = []
while I > 0
a. If PARTS[I] = "node_modules" CONTINUE
b. DIR = path join(PARTS[0...I] + "node_modules")
c. DIRS = DIRS + DIR
d. let I = I - 1
- return DIRS + GLOBAL_FOLDERS
//
F. LOAD_PACKAGE_IMPORTS(X, DIR) - 处理 package.json 含有属性 "imports" 的场景
Find the closest package scope SCOPE to DIR
If node scope was found, reutrn.
If the SCOPE/packages.json "imports" is null or undefined, return.
let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE), ["node", "require"]) defined in the ESM resolver.
RESOLVE_ESM_MATCH(MATCH)
//
G. LOAD_PACKAGE_EXPORTS(X, DIR) - 处理 package.json 含有属性 "exports" 的场景
- Try to interpret X as a combination of NAME and SUBPATH where the name
may have a @scope/ prefix and the subpath begins with a slash (/
).
- If X does not match this pattern or DIR/NAME/package.json is not a file,
return.
Parse DIR/NAME/package.json, and look for "exports" field.
If "exports" is null or undefined, return.
let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
package.json
"exports", ["node", "require"]) defined in the ESM resolver.
- RESOLVE_ESM_MATCH(MATCH)
H. LOAD_PACKAGE_SELF(X, DIR)
// 处理 package.json 含属性 "exports" 的场景,在 package.json 所在的作用域内,引入 "exports" 的包,也就是加载自己包内的内容;
// 相当于用引用包的方式去引入包自己 和 包内 exports 的模块;
Find the closest package scope SCOPE to DIR.
If no scope was found, return.
If the SCOPE/package.json "exports" is null or undefined, return.
If the SCOPE/package.json "name" is not the first segment of X, return.
let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE),
"." + X.slice("name".length), package.json
"exports", ["node", "require"])
defined in the ESM resolver.
I. RESOLVE_ESM_MATCH(MATCH)
// 处理匹配到的 esm 模块;找模块的具体路径;然后 根据文件扩展名取加载它们;
RESOLVE_ESM_MATCH(MATCH)
let RESOLVED_PATH = fileURLToPath(MATCH)
If the file at RESOLVED_PATH exists, load RESOLVED_PATH as its extension format. STOP
THROW "not found"
官方的总结
要获取 require() 时将加载的确切文件名,则使用 require.resolve() 函数。
综上所述,这里是 require() 的伪代码高级算法:
require(X) from module at path Y
1. If X is a core module,
a. return the core module
b. STOP
2. If X begins with '/'
a. set Y to be the file system root
3. If X begins with './' or '/' or '../'
a. LOAD_AS_FILE(Y + X)
b. LOAD_AS_DIRECTORY(Y + X)
c. THROW "not found"
4. If X begins with '#'
a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
5. LOAD_PACKAGE_SELF(X, dirname(Y))
6. LOAD_NODE_MODULES(X, dirname(Y))
7. THROW "not found"
LOAD_AS_FILE(X)
1. If X is a file, load X as its file extension format. STOP
2. If X.js is a file, load X.js as JavaScript text. STOP
3. If X.json is a file, parse X.json to a JavaScript Object. STOP
4. If X.node is a file, load X.node as binary addon. STOP
LOAD_INDEX(X)
1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
3. If X/index.node is a file, load X/index.node as binary addon. STOP
LOAD_AS_DIRECTORY(X)
1. If X/package.json is a file,
a. Parse X/package.json, and look for "main" field.
b. If "main" is a falsy value, GOTO 2.
c. let M = X + (json main field)
d. LOAD_AS_FILE(M)
e. LOAD_INDEX(M)
f. LOAD_INDEX(X) DEPRECATED
g. THROW "not found"
2. LOAD_INDEX(X)
LOAD_NODE_MODULES(X, START)
1. let DIRS = NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
a. LOAD_PACKAGE_EXPORTS(X, DIR)
b. LOAD_AS_FILE(DIR/X)
c. LOAD_AS_DIRECTORY(DIR/X)
NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
a. if PARTS[I] = "node_modules" CONTINUE
b. DIR = path join(PARTS[0 .. I] + "node_modules")
c. DIRS = DIR + DIRS
d. let I = I - 1
5. return DIRS + GLOBAL_FOLDERS
LOAD_PACKAGE_IMPORTS(X, DIR)
1. Find the closest package scope SCOPE to DIR.
2. If no scope was found, return.
3. If the SCOPE/package.json "imports" is null or undefined, return.
4. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE),
["node", "require"]) defined in the ESM resolver.
5. RESOLVE_ESM_MATCH(MATCH).
LOAD_PACKAGE_EXPORTS(X, DIR)
1. Try to interpret X as a combination of NAME and SUBPATH where the name
may have a @scope/ prefix and the subpath begins with a slash (`/`).
2. If X does not match this pattern or DIR/NAME/package.json is not a file,
return.
3. Parse DIR/NAME/package.json, and look for "exports" field.
4. If "exports" is null or undefined, return.
5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
`package.json` "exports", ["node", "require"]) defined in the ESM resolver.
6. RESOLVE_ESM_MATCH(MATCH)
LOAD_PACKAGE_SELF(X, DIR)
1. Find the closest package scope SCOPE to DIR.
2. If no scope was found, return.
3. If the SCOPE/package.json "exports" is null or undefined, return.
4. If the SCOPE/package.json "name" is not the first segment of X, return.
5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE),
"." + X.slice("name".length), `package.json` "exports", ["node", "require"])
defined in the ESM resolver.
6. RESOLVE_ESM_MATCH(MATCH)
RESOLVE_ESM_MATCH(MATCH)
1. let RESOLVED_PATH = fileURLToPath(MATCH)
2. If the file at RESOLVED_PATH exists, load RESOLVED_PATH as its extension
format. STOP
3. THROW "not found"
理解 package.json "exports" & "imports"
-
imports:
- 引入当前包下的具体文件,如 “./mod/index.js”, 相当于给这个文件重新命名,给这个包内的其它模块使用;
// my-package/mod/index.js const path = require('path') const filePath = path.resolve(__dirname, __filename) module.exports = { filePath } // my-package/package.json { "imports": { "#mod": { "default": "./mod/index.js" } } } // my-package/main.js const mod = require('#mod') console.log('mod ->: ',mod) // command $ node main.js // 结果: mod ->: { filePath: '/...../my-package/mod/index.js' }
- 也可以引入第三方包, 这个可以是外部包,如 "lodash",相当于给 lodash 包引入进来,又换了名字;供包内模块使用;
// my-package/package.json { "imports": { "#lod": "lodash" } } // my-package/main.js const lod = require('#lod') console.log(lod.toString([2, 3, 4])) // command $ node main.js // 结果:1,2,3
-
exports:
"exports" 是 Node.js 12+ 开始支持的,是 "main" 的替代方案;它俩同时存在的时候,"exports" 的优先级更高;会替换 "main";
支持定义 子路径导出 和 条件导出;
封装内部未导出的模块;
"exports" 中定义的所有路径必须以 "./" 开头的相对文件URL;
package 中的文件也可以引入 package 自己的 package.json 文件中 "exports" 的包;
// my-package/package.json { "exports": { ".": "./lib/index.js", "./baz": "./lib/baz.js", "./foo": "./lib/foo.js" } } // my-package/main.js const t5 = require('test-5') const baz = require('test-5/baz') // my-package 被 install 到 node_modules 中 // 路径定义为: my-project/node_modules/my-package // my-project/main.js const t5 = require('test-5') const baz = require('test-5/baz')