Node.js模块加载

Node 模块加载类型:

  1. 加载 Node核心模块

require(X)

  1. 加载 工程师自己编写的本地模块

require(./X) // 相对路径

require(../X) // 相对路径

require(/X) // 系统根目录,不常用

require(./X.[extention])

require(../X.[extention])

require(/X.[extention])

  1. 加载 安装的第三方模块

require(X)

Node 模块加载顺序

以 require(X) 为例:

先了解几个文件查找过程:

LOAD_AS_FILE(X):

  1. 查找 X.js 文件

  2. 查找 X.json 文件

  3. 查找 X.node 文件

LOAD_AS_INDEX(X):

  1. 查找 X/index.js 文件

  2. 查找 X/index.json 文件

  3. 查找 X/index.node 文件

LOAD_AS_DIRECTOR(X):

  1. 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"

  2. X/package.json 文件不存在

a. 执行 LOAD_INDEX(X), 即 foo 文件夹下的 index 文件

LOAD_NODE_MODULES(X, START)


以 reuqire(X) 为例:

a. 如果 X 是以 '/'、'./'、'../' 开头,证明加载的是本地模块;

  1. X 没有后缀名,会按照以下顺序进行查找,找到后进行加载;
i. X 作为文件 查找,执行 LOAD_AS_FILE(X) 

ii. X 作为文件夹路径 查找,执行 LOAD_AS_DIRECTOR(X) 

iii. THROW "not found"
  1. X 有后缀名,会直接查找 X文件;
i. 找到进行加载; 

ii. 找不到 THROW "not found"; 

b. 如果 X 不是 以 '/'、'./'、'../' 开头,那么证明要加载的 不是本地模块;

  1. Node 首先会检索核心模块,找到核心模块X,加载 Node 核心模块X,STOP;

  2. 没找到核心模块X,那么会检索第三方库;即,node_modules 中的库;找到 第三方库X,加载库X;STOP;

Node 如何加载的第三方包:

首先,会在当前文件目录去寻找 node_modules 文件夹,从里边寻找 X 模块,如果没有找到会一次到上级目录中 继续寻找 node_modules 中的 X 模块;

依次类推,知道到找到系统根目录为止;

当嵌套的层次很深时,这个文件查找列表可能会变的很长。因此,在查找时做了如下优化:

  1. /node_modules 不会附加在一个以 /node_modules 结尾的文件夹后面。

demo:

/home/foo/bar/node_modules/node_modules/xx.js

如上的 /node_modules/node_modules 的这种情况是不会出现的;

  1. 如果调用 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) - 加载文件

  1. foo.js 存在, load foo.js

  2. foo.js 不存在, load foo.json

  3. foo.json 不存在, load foo.node

  4. foo.node 不存在,THROW "not found"

//
B. LOAD_INDEX(X) - 加载入口文件

  1. load foo/index.js

  2. foo/index.js 不存在, load foo/index.json

  3. foo/index.json 不存在, load foo/index.node

  4. foo/index.node 不存在, THROW "not found"

//
C. LOAD_AS_DIRECTORY(X) - 加载文件夹

  1. 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"

  2. foo/package.json 文件不存在

a. 执行 LOAD_INDEX(X), 即 foo 文件夹下的 index 文件

//
D. LOAD_NODE_MODULES(X, STRAT) - 加载 node模块
// 注意这里的 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)

//

E. NODE_MODULES_PATHS(START) - 列出所有可能的 node_modules 路径

  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 = DIRS + DIR

d. let I = I - 1

  1. return DIRS + GLOBAL_FOLDERS

//

F. LOAD_PACKAGE_IMPORTS(X, DIR) - 处理 package.json 含有属性 "imports" 的场景

  1. Find the closest package scope SCOPE to DIR

  2. If node scope was found, reutrn.

  3. If the SCOPE/packages.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)

//

G. LOAD_PACKAGE_EXPORTS(X, DIR) - 处理 package.json 含有属性 "exports" 的场景

  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 (/).

  1. If X does not match this pattern or DIR/NAME/package.json is not a file,

return.

  1. Parse DIR/NAME/package.json, and look for "exports" field.

  2. If "exports" is null or undefined, return.

  3. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,

package.json "exports", ["node", "require"]) defined in the ESM resolver.

  1. RESOLVE_ESM_MATCH(MATCH)

H. LOAD_PACKAGE_SELF(X, DIR)
// 处理 package.json 含属性 "exports" 的场景,在 package.json 所在的作用域内,引入 "exports" 的包,也就是加载自己包内的内容;
// 相当于用引用包的方式去引入包自己 和 包内 exports 的模块;

  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.

I. RESOLVE_ESM_MATCH(MATCH)

// 处理匹配到的 esm 模块;找模块的具体路径;然后 根据文件扩展名取加载它们;

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"

官方的总结

要获取 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:

    1. 引入当前包下的具体文件,如 “./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'
    
    }
    
    
    1. 也可以引入第三方包, 这个可以是外部包,如 "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:

    1. "exports" 是 Node.js 12+ 开始支持的,是 "main" 的替代方案;它俩同时存在的时候,"exports" 的优先级更高;会替换 "main";

    2. 支持定义 子路径导出 和 条件导出;

    3. 封装内部未导出的模块;

    4. "exports" 中定义的所有路径必须以 "./" 开头的相对文件URL;

    5. 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')
    
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容