CommonJS

CommonJS 是由 JavaScript 社区于 2oo9 年提出的包含模块、文件、IO、控制台在内的一系列标准。Node.js 的实现中采用了 CommonJS 标准的一部分,并在其基础上进行了一些调整。我们所说的 CommonJS 模块和 Node.js 中的实现并不完全一样,现在一般谈到 CommonJS 其实是 Node.js 中的版本,而非它的原始定义。

CommonJS 最初只为服务端而设计,直到有了 Browserify ——一个运行在 Node.js 环境下的模块打包工具,它可以将 CommonJS 模块打包为浏览器可以运行的单个文件。这意味着客户端的代码也可以遵循 CommonJS 标准来编写了。

不仅如此,借 Node.js 的包管理器,npm 开发者还可以获取他人的代码库,或者把自己的代码发布上去供他人使用。这种可共享的传播方式使 CommonJS 在前端开发领域逐渐流行起来。

模块

通过 script 标签插入的 JS 文件的顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境:而通过 CommonJS 引入的 JS 文件会形成一个属于模块自身的作用域,所有的变量及函数只有自己能访问,对外是不可见的。请看下面的例子:

//calculator.js
var name = 'calculator.js';
//index.js
var name = 'index.js';
require('./calculator.js');
console.log(name); // index.js

这里有两个文件,在 index.js 中我们通过 CommonJS 的 require 函数加载 calculator.js。运行之后控制台结果是 “index.js”,说明 calculator.js 中的变量声明并不会影响 index.js,可见每个模块是拥有各自的作用域的。

导出

导出是一个模块向外暴露自身的唯一方式。在 CommonJS 中,通过 module.exports 可以导出模块中的内容,如:

module.exports = {
    name: 'calculater'
    add: function(a, b){
        return a + b;
    }
}

CommonJS 模块内部会用一个 module 对象存放当前模块的信息,可以理解成在每个模块的最开始定义了以下对象:

var module = {//...};
// 模块自身逻辑
module.exports = {//...};

module.exports 用来指定该模块要对外暴露哪些内容,为了书写方便,CommonJS 也支持另种简化的导出方式——直接使用 exports:

exports.name = 'calculater';
exports.add = function(a,b){
	return a + b;
}

其内在机制是将 exports 指向 module.exports,而 module.exports 在初始化时是一个空对象。我们可以简单地理解为,CommonJS 在每个模块的首部默认添加了以下代码:

var module = {
	exports: {}
};
var exports = module.exports;

但不能直接给 exports 赋值,否则会导致其失效。还有禁止将 module.exports 和 exports 混用。

导入

在 CommonJS 中使用 require 语法进行模块导入。如:

// calculator.js
module.exports = {	
    add: function(a, b){
        return a + b;
    }
}
// index.js
const calculator = require('./calculator.js');
const sum = calculator.add(2, 3);
console.log(sum); // 5

当我们使用 require 导入一个模块时会有两种情况:

  • 该模块未曾被加载过。这时会首先执行该模块,然后获取到该模块最终导出的内容。
  • 该模块已经被加我过。这时该模块的代码不会再次执行,而是直接获取该模块上一次导出的内容。

我们前面提到,模块会有一个 module 为象用来存放其信息,这个对象中有一个属性 loaded 用于记录该模块是否被加载过。loaded 的值默认为 false ,在模块第一次被加载和执行过后会置为true,后面再次加载时检查到 module.loadedtrue ,则不会再次执行模块代码。

有时我们加载一个模块,不需要获取其导出的内容,只是想要通过执行它而产生某种作用,比如把它的接口挂在全局对象上,此时直接使用 require 即可。

require('./task.js');

模块封装器

在执行模块代码之前,Node.js 将使用如下所示的函数封装器对其进行封装:

(function(exports, require, module, __filename, __dirname) {
    // Module code actually lives in here
}); 

通过这样做,Node.js 实现了以下几点:

  • 它将顶层变量(使用 varconstlet 定义)保持在模块而不是全局对象的范围内。
  • 它有助于提供一些实际特定于模块的全局变量,例如:
    • moduleexports 对象,实现者可以用来从模块中导出值。
    • 便利变量 __filename__dirname,包含模块的绝对文件名和目录路径。

模块作用域

__dirname

当前模块的目录名。这与 __filenamepath.dirname() 相同。

__filename

当前模块的文件名。这是当前模块文件的已解析符号链接的绝对路径。对于主程序,这不一定与命令行中使用的文件名相同。

在这里插入图片描述

exports

module.exports 的引用,其输入更短。

在这里插入图片描述

直接给 module.exportsexports 赋值都会断开引用关系。

在这里插入图片描述

在这里插入图片描述

直接赋值会导致以下后果:

在这里插入图片描述

moudle

当前模块的引用,请参阅关于 module 对象的部分。特别是,module.exports 用于定义模块导出的内容,并通过 require() 使其可用。

require(id: string)

  • id —— 模块名称或路径
  • 返回值—— 模块导出的内容

用于导入模块、JSON 和本地文件。模块可以从 node_modules 导入。可以使用相对路径(例如 ././foo./bar/baz../foo)导入本地模块和 JSON 文件,该路径将根据 __dirname(如果有定义)命名的目录或当前工作目录进行解析。

require.cache

  • 第一次加载某个模块时,Node 会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的 module.exports 属性(不会再次执行该模块)。
  • 如果需要多次执行模块中的代码,一般可以让模块暴露行为(函数)。
  • 模块的缓存可以通过 require.cache 拿到,也可以从此对象中删除键值,下一次 require 将重新加载模块。

在这里插入图片描述

require.main

Module 对象代表 Node.js 进程启动时加载的入口脚本,如果程序的入口点不是 CommonJS 模块,则为 undefined。参见访问主模块。

在这里插入图片描述

require.resolve(request[, options])

  • request —— 要解析的模块路径。
  • options
    • paths 从中解析模块位置的路径。如果存在,将使用这些路径而不是默认解析路径,但 GLOBAL_FOLDERS$HOME/.node_modules 除外,它们始终包含在内。这些路径中的每一个都用作模块解析算法的起点,这意味着从此位置检查 node_modules 层级。

在这里插入图片描述

require.resolve.paths(request)
  • request—— 正在检索其查找路径的模块路径。
  • 返回一个数组,其中包含在解析请求期间搜索的路径,如果请求字符串引用核心模块,例如 httpfs,则返回 null

在这里插入图片描述

moudle 对象

在每个模块中,module 自由变量是对代表当前模块的对象的引用。module 实际上不是全局的,而是每个模块本地的。

在这里插入图片描述

module.children

此模块首次需要的模块对象。

在这里插入图片描述

module.exports

从模块中导出任何你想导出的东西。
在这里插入图片描述

赋值给 module.exports 必须立即完成。不能在任何回调中完成。以下不起作用:

在这里插入图片描述

exports

参考 模块作用域->exports

module.filename

模块的完全解析文件名。

在这里插入图片描述

module.id

模块的标识符。通常这是完全解析的文件名。

在这里插入图片描述

module.isPreloading

如果模块在 Node.js 预加载阶段运行,则为 true

在这里插入图片描述

module.loaded

模块是否已完成加载,或正在加载。

在这里插入图片描述

module.parent

第一个需要此模块的模块,如果当前模块是当前进程的入口点,则为 null;如果该模块是由非 CommonJS 模块加载的模块(例如:REPL 或 import),则为 undefined

在这里插入图片描述

module.path

模块的目录名称。这通常与 module.idpath.dirname() 相同。

在这里插入图片描述

module.paths

模块的搜索路径。

在这里插入图片描述

module.require(id)

  • id —— 模块名称或路径
  • 返回值—— 模块导出的内容

module.require() 方法提供了一种加载模块的方法,就像从原始模块调用 require() 一样。为了做到这一点,有必要获取对模块对象的引用。由于 require() 返回 module.exports,并且该模块通常仅在特定模块的代码中可用,因此必须显式导出才能使用。

在这里插入图片描述

在这里插入图片描述

基本上,用 module.requrie 是极少见的,借用它家的 module 更为少见。

module.builtinModules

Node.js 提供的所有模块的名称列表。可用于验证模块是否由第三方维护。

注意:列表不包含像 node:test 这样的仅前缀模块。

此上下文中的 module 与模块封装器提供的对象不同。要访问它,需要 Module 模块:

// import { builtinModules } from 'node:module' // es模块
const builtin = require('node:module').builtinModules;

在这里插入图片描述

module.createRequire(filename)

  • filename —— 用于构造 require 函数的文件名。必须是文件网址对象、文件网址字符串、或绝对路径字符串。
  • 返回 —— require 函数。

在这里插入图片描述

module.isBuiltin(moduleName)

  • moduleName —— 模块名称。
  • 返回:—— 如果模块是内置的,则返回 true,否则返回 false

在这里插入图片描述

module.register(specifier[, parentURL] [, options])

  • specifier: <string>|<URL> —— 需要注册的定制钩子;这应该与传递给 import() 的字符串相同,但如果它是相对的,则它是相对于 parentURL 解析的。
  • parentURL: <string>|<URL> —— 如果你想要相对于基本 URL(例如 import.meta.url)解析 specifier,你可以在此处传递该 URL。默认值:'data:'
  • options: <Object>
    • parentURL: <string>|<URL> —— 如果你想要相对于基本 URL(例如 import.meta.url)解析 specifier,你可以在此处传递该 URL。如果 parentURL 作为第二个参数提供,则忽略此属性。默认值:'data:'
    • data: <any> —— 传递到 initialize 钩子的任何任意的、可克隆的 JavaScript 值。
    • transferList: <Object[]> —— 可转换对象 要传递到 initialize 钩子中。

注册一个导出钩子的模块,用于自定义 Node.js 模块解析和加载行为。参见定制钩子

module.syncBuiltinESMExports()

module.syncBuiltinESMExports() 方法,用于更新所有内置 ES 模块的实时绑定,以匹配 CommonJS 模块的exports属性。这个方法的主要目的是为了协调 CommonJS 和 ES6 模块之间的交互,确保在使用混合模块系统时,数据的同步和一致性。

此外,这个方法还涉及到对内置模块的修改和同步。虽然内置模块在运行时可以被修改,但不能新增或删除。通过module.syncBuiltinESMExports(),可以确保这些修改被正确地同步到整个模块系统中,保持数据的一致性和完整性。

在这里插入图片描述

确定模块系统

当传递给 node 作为初始输入时,或者当被 import 语句或 import() 表达式引用时,Node.js 会将以下内容视为ES 模块

  • 扩展名为 .mjs 的文件。
  • 当最近的父 package.json 文件包含值为 "module" 的顶层 "type" 字段时,扩展名为 .js 的文件。
  • 字符串作为参数传入 --eval,或通过 STDIN 管道传输到 node,带有标志 --input-type=module
  • 使用 --experimental-detect-module 时,包含语法的代码仅成功解析为ES 模块,例如 importexport 语句或 import.meta,没有明确标记应如何解释它。显式标记是 .mjs.cjs 扩展、带有 "module""commonjs" 值的 package.json "type" 字段,或者 --input-type--experimental-default-type 标志。CommonJS 或 ES 模块都支持动态 import() 表达式,并且不会导致文件被视为 ES 模块。

当传递给 node 作为初始输入时,或者当被 import 语句或 import() 表达式引用时,Node.js 会将以下内容视为CommonJS

  • 扩展名为 .cjs 的文件。
  • 当最近的父 package.json 文件包含值为 "commonjs" 的顶层字段 "type" 时,则扩展名为 .js 的文件。
  • 字符串作为参数传入 --eval--print,或通过 STDIN 管道传输到 node,带有标志 --input-type=commonjs

除了这些明确的情况之外,还有其他情况,Node.js 根据 --experimental-default-type 标志的值默认使用一个模块系统或另一个模块系统:

  • 如果同一文件夹或任何父文件夹中不存在 package.json 文件,则以 .js 结尾或没有扩展名的文件。
  • 如果最近的父 package.json 字段缺少 "type" 字段,则以 .js 结尾或没有扩展名的文件;除非该文件夹位于 node_modules 文件夹内。(当 package.json 文件缺少 "type" 字段时,无论 --experimental-default-type 如何,为了向后兼容,node_modules 下的包范围始终被视为 CommonJS。)
  • 当未指定 --input-type 时,字符串作为参数传递给 --eval 或通过 STDIN 通过管道传递给 node

该标志当前默认为 "commonjs",但将来可能会更改为默认为 "module"。因此,最好尽可能明确;特别是,包作者应始终在其 package.json 文件中包含 "type" 字段,即使在所有源都是 CommonJS 的包中也是如此。如果 Node.js 的默认类型发生变化,显式说明包的 type 将使包面向未来,它还将使构建工具和加载器更容易确定应如何解释包中的文件。

启用

Node.js 有两个模块系统:CommonJS 模块和 ECMAScript 模块

默认情况下,Node.js 会将以下内容视为 CommonJS 模块:

  • 扩展名为 .cjs 的文件;
  • 当最近的父 package.json 文件包含值为 "commonjs" 的顶层字段 "type" 时,则扩展名为 .js 的文件。
  • 当最近的父 package.json 文件不包含顶层字段 "type" 或任何父文件夹中都没有 package.json 时,具有 .js 扩展名或不带扩展名的文件;除非该文件包含错误的语法,除非它被评估为 ES 模块。包作者应该包括 "type" 字段,即使在所有源都是 CommonJS 的包中也是如此。明确包的 type 将使构建工具和加载器更容易确定包中的文件应该如何解释。
  • 扩展名不是 .mjs.cjs.json.node.js 的文件(当最近的父 package.json 文件包含值为 "module" 的顶层字段 "type" 时,这些文件将被识别为 CommonJS 模块只有当它们是通过 require() 包含,而不是用作程序的命令行入口点时)。

有关详细信息,请参阅确定模块系统

调用 require() 始终使用 CommonJS 模块加载器。调用 import() 始终使用 ECMAScript 模块加载器。

访问主模块

当文件直接从 Node.js 运行时,则 require.main 被设置为其 module。这意味着可以通过测试 require.main === module 来确定文件是否被直接运行。

对于文件 foo.js,如果通过 node foo.js 运行,则为 true,如果通过 require('./foo') 运行,则为 false

当入口点不是 CommonJS 模块时,则 require.mainundefined,且主模块不可达。

在这里插入图片描述

### CommonJS 模块化规范的使用方法与示例 #### 一、基本概念 CommonJS 是一种模块化规范,主要用于服务器端 JavaScript 开发环境(如 Node.js)。它规定了模块的特性及其相互依赖的方式。通过该规范,开发者能够将代码分割成多个独立的小型模块,并按需加载这些模块[^1]。 #### 二、导出模块 (`module.exports`) 在 CommonJS 中,`module.exports` 对象用于向外暴露当前模块的功能或数据。任何赋值给 `module.exports` 的内容都可以被其他文件导入并使用。 以下是导出模块的一个简单示例: ```javascript // mathUtils.js function add(a, b) { return a + b; } function subtract(a, b) { return a - b; } module.exports = { add, subtract }; // 将函数对象导出 ``` 上述代码创建了一个名为 `mathUtils.js` 的模块,并通过 `module.exports` 向外提供了两个功能:加法和减法操作[^2]。 #### 三、导入模块 (`require()`) 要使用另一个模块中的功能,可以通过 `require()` 方法来引入目标模块的内容。`require()` 函数会返回由目标模块导出的对象。 以下是如何导入前面定义的 `mathUtils.js` 并调用其中的方法: ```javascript // app.js const mathUtils = require('./mathUtils'); // 引入本地模块 console.log(mathUtils.add(5, 3)); // 输出: 8 console.log(mathUtils.subtract(5, 3)); // 输出: 2 ``` 这里需要注意的是,在路径前加上 `./` 表明这是一个相对路径指向的本地模块;如果省略,则表示尝试加载内置模块或安装到项目中的第三方包[^3]。 #### 四、`exports` 和 `module.exports` 虽然两者看起来相似,但实际上存在细微差别: - **`exports`** 是 `module.exports` 的一个快捷方式,默认情况下它们都指向同一个对象。 - 如果重新分配整个 `module.exports` 值,则不会影响原来的 `exports` 变量。 下面是一个例子展示两者的差异: ```javascript // 错误示范 (仅修改 exports) exports = function greet() { console.log('Hello!'); }; // 正确做法 (修改 module.exports) module.exports = function greet() { console.log('Hello!'); }; ``` 只有当直接改变 `module.exports` 而不是它的属性时才有效果[^4]。 --- ### 总结 CommonJS 提供了一套简洁有效的机制支持模块间的分离与协作,极大地促进了大型项目的构建与发展过程。利用 `module.exports` 进行输出以及借助于 `require()` 实现输入构成了这一标准的核心部分。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值