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"这样的输出。

  1. const ordinal = require("ordinal");
  2. const {days, months} = require("date-names");
  3. exports.formatDate = function(date, format) {
  4. return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag => {
  5. if (tag == "YYYY") return date.getFullYear();
  6. if (tag == "M") return date.getMonth();
  7. if (tag == "MMMM") return months[date.getMonth()];
  8. if (tag == "D") return date.getDate();
  9. if (tag == "Do") return ordinal(date.getDate());
  10. if (tag == "dddd") return days[date.getDay()];
  11. });
  12. };

ordinal的接口是单个函数,而date-names导出包含多个东西的对象 - daysmonths是名称数组。 为导入的接口创建绑定时,解构是非常方便的。

该模块将其接口函数添加到exports,以便依赖它的模块可以访问它。 我们可以像这样使用模块:

  1. const {formatDate} = require("./format-date");
  2. console.log(formatDate(new Date(2017, 9, 13),
  3. "dddd the Do"));
  4. // → Friday the 13th

我们可以用最简单的形式定义require,如下所示:

  1. require.cache = Object.create(null);
  2. function require(name) {
  3. if (!(name in require.cache)) {
  4. let code = readFile(name);
  5. let module = {exports: {}};
  6. require.cache[name] = module;
  7. let wrapper = Function("require, exports, module", code);
  8. wrapper(require, module.exports, module);
  9. }
  10. return require.cache[name].exports;
  11. }

在这段代码中,readFile是一个构造函数,它读取一个文件并将其内容作为字符串返回。标准的 JavaScript 没有提供这样的功能,但是不同的 JavaScript 环境(如浏览器和 Node.js)提供了自己的访问文件的方式。这个例子只是假设readFile存在。

为了避免多次加载相同的模块,require需要保存(缓存)已经加载的模块。被调用时,它首先检查所请求的模块是否已加载,如果没有,则加载它。这涉及到读取模块的代码,将其包装在一个函数中,然后调用它。

我们之前看到的ordinal包的接口不是一个对象,而是一个函数。 CommonJS 模块的特点是,尽管模块系统会为你创建一个空的接口对象(绑定到exports),但你可以通过覆盖module.exports来替换它。许多模块都这么做,以便导出单个值而不是接口对象。

通过将requireexportsmodule定义为生成的包装函数的参数(并在调用它时传递适当的值),加载器确保这些绑定在模块的作用域中可用。

提供给require的字符串翻译为实际的文件名或网址的方式,在不同系统有所不同。 当它以"./""../"开头时,它通常被解释为相对于当前模块的文件名。 所以"./format-date"就是在同一个目录中,名为format-date.js的文件。

当名称不是相对的时,Node.js 将按照该名称查找已安装的包。 在本章的示例代码中,我们将把这些名称解释为 NPM 包的引用。 我们将在第 20 章详细介绍如何安装和使用 NPM 模块。

现在,我们不用编写自己的 INI 文件解析器,而是使用 NPM 中的某个:

  1. const {parse} = require("ini");
  2. console.log(parse("x = 10\ny = 20"));
  3. // → {x: "10", y: "20"}