1. 背景
Node.js 是一个基于 V8 引擎的 JavaScript 运行环境,它可以使我们在服务器端构建高性能的网络应用程序。
在 Node.js 中,模块化是一个很重要的概念,因为它的模块化系统支持很多种不同的模块加载方式,而 CommonJS(以下简称 cjs)模块是其中最为常用的一种。
2. cjs 模块的加载过程
2.1 require 函数
在 Node.js 中,我们使用 require 函数来加载一个模块。
require 函数的定义如下:
function require(path) {
// ...
}
它接收一个参数:模块的路径,返回该模块导出的对象。
当我们在一个模块中调用 require 函数时,Node.js 会在特定的几个路径下查找该模块的源文件,并执行该文件。
通常,一个模块的路径可以是相对路径或绝对路径。当我们传入一个相对路径时,Node.js 会根据该模块的位置进行查找,例如:
const foo = require('./foo');
在上面的例子中,Node.js 会在当前模块的目录下查找 foo 模块。
当我们传入一个绝对路径时,Node.js 会直接使用该路径进行查找,例如:
const bar = require('/Users/username/projects/bar');
在上面的例子中,Node.js 会直接使用绝对路径 /Users/username/projects/bar 进行查找 bar 模块。
2.2 module 对象
在一个模块的源文件中,我们可以访问 module 对象。
module 对象是 Node.js 为每个模块创建的一个对象,其中包含该模块的一些信息,例如该模块的文件名、导出的对象等。
在一个模块的源文件中,我们可以通过在文件顶部添加如下代码来访问 module 对象:
console.log(module.filename); // 当前模块的文件名
console.log(module.exports); // 当前模块的导出对象
需要注意的是,module.exports 对象是每个模块导出的对象,该对象的内容会被 require 函数返回给调用方。
2.3 匿名包装函数
在 Node.js 的实现中,每个模块都被包装在一个匿名包装函数中。
这个函数接收 5 个参数:exports、require、module、__filename、__dirname,分别对应导出对象、require 函数、module 对象、当前模块的文件名、当前模块的目录名。
当我们在一个模块中定义变量或函数时,它们都会成为该匿名包装函数的局部变量,不能被其他模块直接访问。
例如,我们在一个模块中定义如下变量:
const x = 1;
对于其他模块来说,变量 x 是不可见的。
2.4 cjs 模块的加载过程
cjs 模块的加载过程可以大致分为以下几个步骤:
解析模块的路径
查找模块的源文件
读取模块的源文件
将模块的代码包装在一个匿名包装函数中
执行匿名包装函数,返回该模块的导出对象
下面我们来详细地说明一下这些步骤。
2.4.1 解析模块的路径
当我们在一个模块中调用 require 函数时,Node.js 会首先根据该模块的路径解析出该模块的绝对路径。
例如,当我们在一个模块中调用 require('./foo') 时,Node.js 会根据当前模块的目录和 './foo' 得出 foo 模块的绝对路径,例如:
const path = require('path');
const filename = module.filename;
const basepath = path.dirname(filename);
const filepath = path.resolve(basepath, './foo');
需要注意的是,模块的解析规则是可以被修改的。在 Node.js 中,可以通过设置 module.paths 属性来修改模块的查找路径,例如:
module.paths.push('/my/path');
上面的代码会将 /my/path 添加到模块的查找路径中,使 Node.js 在查找模块时也会在该目录下查找模块。
2.4.2 查找模块的源文件
当 Node.js 解析出模块的绝对路径后,它会在一系列路径中查找该模块的源文件,具体的路径列表是通过 module.paths 属性定义的。
在默认情况下,Node.js 会在如下的路径列表中查找模块的源文件:
当前模块的目录
当前模块的父目录
当前模块的上级目录
...
全局模块目录
需要注意的是,Node.js 提供了多种方式来修改模块的查找路径,例如:
添加 NODE_PATH 环境变量
修改 require.main.paths 属性
在 require 函数中指定 paths 参数
2.4.3 读取模块的源文件
当 Node.js 找到一个模块的源文件后,它会读取该文件的内容,并将其包装在一个匿名包装函数中。
具体来说,Node.js 会使用 fs 模块中的 fs.readFileSync 函数来读取模块的源文件,例如:
const fs = require('fs');
const content = fs.readFileSync(filepath, 'utf8');
2.4.4 包装模块的代码
当 Node.js 读取模块的源文件后,它会使用一个模板函数来将源代码包装在一个匿名包装函数中,例如:
(function(exports, require, module, __filename, __dirname) {
// 模块的源代码
});
需要注意的是,在模块中定义的变量和函数都会成为该匿名包装函数的局部变量。
例如,当我们在一个模块中定义如下函数时:
function foo() { return 'foo'; }
该函数是成为匿名包装函数的局部变量,其他模块无法访问它。
2.4.5 执行匿名包装函数
当 Node.js 将模块的代码包装在了一个匿名包装函数中后,它会立即执行该函数,并将函数的返回值作为模块的导出对象。
例如,当我们在一个模块中导出如下对象时:
module.exports = { foo: 'bar' };
该对象会被读取并返回给 require 函数调用方。
需要注意的是,由于每个模块都是通过一个匿名包装函数来进行包装和执行的,在一个模块中定义的变量和函数都是该函数的局部变量,所以其他模块无法直接访问它们。
3. 总结
在 Node.js 中,模块是一个重要的概念,而 cjs 是其中最为常用的一种模块加载方式。
正确地理解 cjs 模块的加载过程对于编写高质量的 Node.js 应用程序至关重要。
在本文中,我们介绍了 cjs 模块的加载过程,包括 require 函数、module 对象、匿名包装函数以及模块的查找规则等内容。
希望本文能够对大家理解 Node.js 的模块化有所帮助。