CommonJS
用于连接 JavaScript 模块的最广泛的方法称为 CommonJS 模块。 Node.js 使用它,并且是 NPM 上大多数包使用的系统。
CommonJS 模块的主要概念是称为require
的函数。 当你使用依赖项的模块名称调用这个函数时,它会确保该模块已加载并返回其接口。
由于加载器将模块代码封装在一个函数中,模块自动得到它们自己的局部作用域。 他们所要做的就是,调用require
来访问它们的依赖关系,并将它们的接口放在绑定到exports
的对象中。
此示例模块提供了日期格式化功能。 它使用 NPM的两个包,ordinal
用于将数字转换为字符串,如"1st"
和"2nd"
,以及date-names
用于获取星期和月份的英文名称。 它导出函数formatDate
,它接受一个Date
对象和一个模板字符串。
模板字符串可包含指明格式的代码,如YYYY
用于全年,Do
用于每月的序数日。 你可以给它一个像"MMMM Do YYYY"
这样的字符串,来获得像"November 22nd 2017"
这样的输出。
const ordinal = require("ordinal");
const {days, months} = require("date-names");
exports.formatDate = function(date, format) {
return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag => {
if (tag == "YYYY") return date.getFullYear();
if (tag == "M") return date.getMonth();
if (tag == "MMMM") return months[date.getMonth()];
if (tag == "D") return date.getDate();
if (tag == "Do") return ordinal(date.getDate());
if (tag == "dddd") return days[date.getDay()];
});
};
ordinal
的接口是单个函数,而date-names
导出包含多个东西的对象 - days
和months
是名称数组。 为导入的接口创建绑定时,解构是非常方便的。
该模块将其接口函数添加到exports
,以便依赖它的模块可以访问它。 我们可以像这样使用模块:
const {formatDate} = require("./format-date");
console.log(formatDate(new Date(2017, 9, 13),
"dddd the Do"));
// → Friday the 13th
我们可以用最简单的形式定义require
,如下所示:
require.cache = Object.create(null);
function require(name) {
if (!(name in require.cache)) {
let code = readFile(name);
let module = {exports: {}};
require.cache[name] = module;
let wrapper = Function("require, exports, module", code);
wrapper(require, module.exports, module);
}
return require.cache[name].exports;
}
在这段代码中,readFile
是一个构造函数,它读取一个文件并将其内容作为字符串返回。标准的 JavaScript 没有提供这样的功能,但是不同的 JavaScript 环境(如浏览器和 Node.js)提供了自己的访问文件的方式。这个例子只是假设readFile
存在。
为了避免多次加载相同的模块,require
需要保存(缓存)已经加载的模块。被调用时,它首先检查所请求的模块是否已加载,如果没有,则加载它。这涉及到读取模块的代码,将其包装在一个函数中,然后调用它。
我们之前看到的ordinal
包的接口不是一个对象,而是一个函数。 CommonJS 模块的特点是,尽管模块系统会为你创建一个空的接口对象(绑定到exports
),但你可以通过覆盖module.exports
来替换它。许多模块都这么做,以便导出单个值而不是接口对象。
通过将require
,exports
和module
定义为生成的包装函数的参数(并在调用它时传递适当的值),加载器确保这些绑定在模块的作用域中可用。
提供给require
的字符串翻译为实际的文件名或网址的方式,在不同系统有所不同。 当它以"./"
或"../"
开头时,它通常被解释为相对于当前模块的文件名。 所以"./format-date"
就是在同一个目录中,名为format-date.js
的文件。
当名称不是相对的时,Node.js 将按照该名称查找已安装的包。 在本章的示例代码中,我们将把这些名称解释为 NPM 包的引用。 我们将在第 20 章详细介绍如何安装和使用 NPM 模块。
现在,我们不用编写自己的 INI 文件解析器,而是使用 NPM 中的某个:
const {parse} = require("ini");
console.log(parse("x = 10\ny = 20"));
// → {x: "10", y: "20"}