“JS模块:CJM”的版本间差异

来自Wikioe
跳到导航 跳到搜索
(创建页面,内容为“category:Node.js == 关于 == 规范的主要内容:模块必须通过 '''module.exports''' 导出对外的变量或接口,通过 '''require()''' 来导入其他模块的输出到当前模块作用域中。 1、所有代码都运行在模块作用域,不会污染全局作用域 —— 一个文件就是一个模块 2、模块初次加载会被缓存,再次使用将从缓存中读取 —— 要让模块再次运行,必须清除缓存 Node.js…”)
 
无编辑摘要
 
(未显示同一用户的3个中间版本)
第1行: 第1行:
[[category:Node.js]]
[[category:JavaScript]] [[category:Node.js]]


== 关于 ==
== 关于<ref>参考:
# [https://nodejs.cn/api/modules.html Node.js文档:CommonJS 模块]
# [https://juejin.cn/post/7108410887052427301 《搞懂前端模块化之 CommonJS 篇》]</ref>==
  规范的主要内容:模块必须通过 '''module.exports''' 导出对外的变量或接口,通过 '''require()''' 来导入其他模块的输出到当前模块作用域中。
  规范的主要内容:模块必须通过 '''module.exports''' 导出对外的变量或接口,通过 '''require()''' 来导入其他模块的输出到当前模块作用域中。
   
   
第92行: 第94行:
# '''module.syncBuiltinESMExports()''':用于更新内置的 ES 模块的所有实时绑定,以匹配 CommonJS 导出的属性。❓
# '''module.syncBuiltinESMExports()''':用于更新内置的 ES 模块的所有实时绑定,以匹配 CommonJS 导出的属性。❓


== 加载机制 ==
== 加载过程 ==
require() 的伪代码高级算法:
要加载的内容可能包括:内置模块、文件模块、第三方模块。
: <syntaxhighlight lang="bash" highlight="">
1. 如果 X 是内置模块
  a. 返回内置模块
  b. 停止执行
2. 如果 X 以 '/' 开头
  a. 设置 Y 为文件根路径
3. 如果 X 以 './' 或 '/' or '../' 开头
  a. LOAD_AS_FILE(Y + X)
  b. LOAD_AS_DIRECTORY(Y + X)
4. LOAD_NODE_MODULES(X, dirname(Y))
5. 抛出异常 "not found"


LOAD_AS_FILE(X)
<big>'''内置模块'''</big>:即“原生模块”,在 Node.js 源代码中定义,位于“lib/”文件夹中
1. 如果 X 是一个文件, 将 X 作为 JavaScript 文本载入并停止执行。
# 解析 <code>module.id</code>:得到 <code>filename</code> 为原生模块标识符(如“fs”);
2. 如果 X.js 是一个文件, X.js 作为 JavaScript 文本载入并停止执行。
# 判断 <code>Module_cache[filename]</code> 是否命中缓存:
3. 如果 X.json 是一个文件, 解析 X.json 为 JavaScript 对象并停止执行。
## 缓存命中:则从缓存中返回该模块的 <code>module.exports</code> —— 【结束加载】
4. 如果 X.node 是一个文件, 将 X.node 作为二进制插件载入并停止执行。
## 缓存未命中:加载该原生模块,然后返回其 <code>module.exports</code> 并缓存 —— 【结束加载】
#* 使用<code>'''node:'''</code>前缀来识别核心模块,可以绕过 require 缓存(即使有该名称的 require.cache 条目);
<big>'''文件模块'''</big>:“相对路径”或“绝对路径”指向的本地模块
# 解析 <code>module.id</code>:得到一个“绝对路径”('''文件 / 目录''');
# 根据“绝对路径”查找“<code>filename</code>”:
## <code>filename</code> 作为“'''文件'''”进行查找:
### 直接命中文件:<code>filename</code> 即为该文件的全解析路径;
### 没有直接命中:为 <code>filename</code> 添加扩展名(<code>.js</code>、<code>.json</code>、<code>.node</code>),再尝试“查找”
## 将 <code>filename</code> 作为“'''目录'''”进行查找:
### 目录下存在“'''package.json'''”文件:根据其“'''main'''”字段得到新的 <code>filename</code>,再尝试“查找”
### 若没有“package.json”文件(或其中没有“main”字段):查找目录下的“'''index'''”文件,将其路径作为<code>filename</code>,再尝试“查找”
##(如果以上皆未能命中)抛出错误 <code>MODULE_NOT_FOUND</code>。
# 判断 <code>Module_cache[filename]</code> 是否命中缓存:
## 缓存命中:则从缓存中返回该模块的 <code>module.exports</code> —— 【结束加载】
## 缓存未命中:加载该原生模块,然后返回其 <code>module.exports</code> 并缓存 —— 【结束加载】
<big>'''第三方模块'''</big>:第三方提供的模块,位于“'''node_modules'''”目录中
# 在“node_modules”查找“<code>filename</code>”:
#* “查找”过程,与【文件模块】类似
#* 从“当前目录的 node_modules”到“根目录的 node_modules”依次查找
#*: <syntaxhighlight lang="bash" highlight="">
[
    '/root/path/to/module/node_modules',
    '/root/path/to/node_modules',
    '/root/path/node_modules',
    '/root/node_modules',
    '/node_modules',
]
</syntaxhighlight>
# 判断 <code>Module_cache[filename]</code> 是否命中缓存:
## 缓存命中:则从缓存中返回该模块的 <code>module.exports</code> —— 【结束加载】
## 缓存未命中:加载该原生模块,然后返回其 <code>module.exports</code> 并缓存 —— 【结束加载】


LOAD_INDEX(X)
== 循环引用 ==
1. 如果 X/index.js 是一个文件,  将 X/index.js 作为 JavaScript 文本载入并停止执行。
对于循环 require() 调用:模块在返回时可能尚未完成执行,但不会出现无限循环。
2. 如果 X/index.json 是一个文件, 解析 X/index.json 为 JavaScript 对象并停止执行。
3. 如果 X/index.node 是一个文件,  将 X/index.node 作为二进制插件载入并停止执行。


LOAD_AS_DIRECTORY(X)
a.js:
1. 如果 X/package.json 是一个文件,
: <syntaxhighlight lang="JavaScript" highlight="">
  a. 解析 X/package.json, 并查找 "main" 字段。
console.log('a starting');
  b. let M = X + (json main 字段)
exports.done = false;
  c. LOAD_AS_FILE(M)
const b = require('./b.js');
  d. LOAD_INDEX(M)
console.log('in a, b.done = %j', b.done);
2. LOAD_INDEX(X)
exports.done = true;
 
console.log('a done');
LOAD_NODE_MODULES(X, START)
</syntaxhighlight>
1. let DIRS=NODE_MODULES_PATHS(START)
b.js:
2. for each DIR in DIRS:
: <syntaxhighlight lang="JavaScript" highlight="">
  a. LOAD_AS_FILE(DIR/X)
console.log('b starting');
  b. LOAD_AS_DIRECTORY(DIR/X)
exports.done = false;
 
const a = require('./a.js');
NODE_MODULES_PATHS(START)
console.log('in b, a.done = %j', a.done);
1. let PARTS = path split(START)
exports.done = true;
2. let I = count of PARTS - 1
console.log('b done');
3. let DIRS = []
</syntaxhighlight>
4. while I >= 0,
main.js:
  a. if PARTS[I] = "node_modules" CONTINUE
: <syntaxhighlight lang="JavaScript" highlight="">
  b. DIR = path join(PARTS[0 .. I] + "node_modules")
console.log('main starting');
  c. DIRS = DIRS + DIR
const a = require('./a.js');
  d. let I = I - 1
const b = require('./b.js');
5. return DIRS
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
</syntaxhighlight>
输出:
: <syntaxhighlight lang="JavaScript" highlight="">
$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true
</syntaxhighlight>
</syntaxhighlight>


== 参考 ==
== 参考 ==
<references/>
<references/>

2023年4月4日 (二) 01:20的最新版本


关于[1]

规范的主要内容:模块必须通过 module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中。

1、所有代码都运行在模块作用域,不会污染全局作用域 —— 一个文件就是一个模块
2、模块初次加载会被缓存,再次使用将从缓存中读取 —— 要让模块再次运行,必须清除缓存

Node.js 将对模块代码封装为如下形式:

(function(exports, require, module, __filename, __dirname) {
// 模块代码实际存在于此处
});
  • 将变量限制在在模块内。
  • 提供了使用模块的属性、方法:
    1. __dirname:当前模块的绝对目录名;
    2. __filename:当前模块的绝对文件名;
    3. exports:对 module.exports 的引用;
    4. module:(对象)对当前模块的引用;
    5. require:(方法)用于导入其他模块(核心模块、文件模块、第三方模块);

exports

exports 变量:是对 module.exports 的引用 —— ❗ 不能对 exports 进行赋值操作,否则只是让其不再对 module.exports 进行引用,而变成一个局部变量。

正确用法:

exports.area = function(x) {console.log(x)};

错误用法:

exports = function(x) {console.log(x)};

module

module 变量:是一个代表当前模块的对象

它提供了以下属性:

  1. module.id:模块的识别符,通常是模块的完全解析文件名。
  2. module.filename:模块的完全解析文件名(带有绝对路径的文件名)。
  3. module.parent:(对象)表示调用该模块的模块。
  4. module.children:(数组)表示该模块用到的其他模块。
  5. module.isPreloading:(布尔值)表示模块是否在 Node.js 预加载阶段运行。
  6. module.loaded:(布尔值)表示模块是否已经完成加载。
  7. module.exports:表示模块对外输出的值。
module.exports 属性表示当前模块对外输出的接口。加载某个模块,其实是加载该模块的 module.exports 属性。不建议同时使用 exports 与 module.exports:可能导致对外输出失效。

示例:

exports.hello = function() {
  return 'hello';
};

module.exports = 'Hello world';
如上,exports 的输出会因 module.exports 重新赋值而失效。

require

require 变量:通常就是模块的 require() 方法

它提供了以下属性、方法:

  1. require(id):用于导入模块、JSON 和本地文件。
    • id 即 module.id(模块识别符), 可以使用相对路径(将根据 __dirname 或当前工作目录进行解析);
  2. require.cache:(对象)缓存对象,模块在需要时缓存在此对象中。
  3. require.main:(对象)代表 Node.js 进程启动时加载的入口脚本。
    • 如果程序的入口点不是 CommonJS 模块,则为 undefined;
  4. require.resolve(request[, options]):查找模块位置,但不加载模块,只返回解析的文件名。
    • request:要解析的模块路径;
    • 如果找不到模块,则会抛出 MODULE_NOT_FOUND 错误;
  5. require.resolve.paths(request):如果 request 字符串引用核心模块(如 http 或 fs),则返回包含在解析 request 或 null 期间搜索的路径的数组。
    • request:要解析的模块路径;

Module

Module 对象:Node 内部的构建函数,提供了当与 Module 的实例交互时提供通用的实用方法

所有模块(module)都是 Module 的实例。

它提供了以下属性、方法:

  1. module.builtinModules:Node.js 提供的所有模块的名称列表。
    • 要访问它,需要 Node 内部的 Module 模块:
      import { builtinModules as builtin } from 'node:module';
      
    • 可用于:验证模块是否由第三方维护;
  2. module.createRequire(filename):用于构造 require 函数的文件名。
    • filename:(string / URL)用于构造 require 函数的文件名(必须是文件网址对象、文件网址字符串、或绝对路径字符串);
  3. module.isBuiltin(moduleName):用于判断一个模块是否为“内置模块”。
    • moduleName:模块名称
  4. module.syncBuiltinESMExports():用于更新内置的 ES 模块的所有实时绑定,以匹配 CommonJS 导出的属性。❓

加载过程

要加载的内容可能包括:内置模块、文件模块、第三方模块。

内置模块:即“原生模块”,在 Node.js 源代码中定义,位于“lib/”文件夹中

  1. 解析 module.id:得到 filename 为原生模块标识符(如“fs”);
  2. 判断 Module_cache[filename] 是否命中缓存:
    1. 缓存命中:则从缓存中返回该模块的 module.exports —— 【结束加载】
    2. 缓存未命中:加载该原生模块,然后返回其 module.exports 并缓存 —— 【结束加载】
    • 使用node:前缀来识别核心模块,可以绕过 require 缓存(即使有该名称的 require.cache 条目);

文件模块:“相对路径”或“绝对路径”指向的本地模块

  1. 解析 module.id:得到一个“绝对路径”(文件 / 目录);
  2. 根据“绝对路径”查找“filename”:
    1. filename 作为“文件”进行查找:
      1. 直接命中文件:filename 即为该文件的全解析路径;
      2. 没有直接命中:为 filename 添加扩展名(.js.json.node),再尝试“查找”
    2. filename 作为“目录”进行查找:
      1. 目录下存在“package.json”文件:根据其“main”字段得到新的 filename,再尝试“查找”
      2. 若没有“package.json”文件(或其中没有“main”字段):查找目录下的“index”文件,将其路径作为filename,再尝试“查找”
    3. (如果以上皆未能命中)抛出错误 MODULE_NOT_FOUND
  3. 判断 Module_cache[filename] 是否命中缓存:
    1. 缓存命中:则从缓存中返回该模块的 module.exports —— 【结束加载】
    2. 缓存未命中:加载该原生模块,然后返回其 module.exports 并缓存 —— 【结束加载】

第三方模块:第三方提供的模块,位于“node_modules”目录中

  1. 在“node_modules”查找“filename”:
    • “查找”过程,与【文件模块】类似
    • 从“当前目录的 node_modules”到“根目录的 node_modules”依次查找
      [
          '/root/path/to/module/node_modules',
          '/root/path/to/node_modules',
          '/root/path/node_modules',
          '/root/node_modules',
          '/node_modules',
      ]
      
  2. 判断 Module_cache[filename] 是否命中缓存:
    1. 缓存命中:则从缓存中返回该模块的 module.exports —— 【结束加载】
    2. 缓存未命中:加载该原生模块,然后返回其 module.exports 并缓存 —— 【结束加载】

循环引用

对于循环 require() 调用:模块在返回时可能尚未完成执行,但不会出现无限循环。

a.js:

console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');

b.js:

console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');

main.js:

console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);

输出:

$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true

参考