Libon

NodeJS require 机制

4 分钟 #JavaScript#nodejs
NodeJS 中 require 方法的机制,及如何对文件的拦截与修改

ToC

usage

在开始之前,可以先看一下基本用法。

1
// 前提代码
2
const resolved = require(X);
3
const Module = require('module');
  1. 如果 X 是内置模块则直接返回模块。
    1. 内置模块可以通过 module.builtinModules 属性来查看。
1
console.log(Module.builtinModules);
  1. 如果 X 模块路径是以 ’/’、’./’、‘../’ 开头

    1. 根据 X 所在的父模块,确定 X 的绝对路径。
    2. 将 X 当成文件,依次查找下面文件,只要有一个存在,则直接返回文件不再查找,可以通过 module._extensions 属性查看受支持的文件类型列表
      • X.js
      • X.json
      • X.node
    3. 将 X 当成目录,依次查找下面文件。只要有一个存在就返回,不再继续执行
      • X/package.json(main字段)
      • X/index.js
      • X/index.json
      • X/index.node
  2. 如果 X 不带路径,比如 ”、 ’.’、’..’ 等。

    1. 根据 X 所在的父模块,确定 X 可能的安装目录。
    2. 依次在每个目录中,将 X 当成文件名或目录名加载。
  3. throw module not found.

require

每个实例都有一个 require 方法。

1
Module.prototype.require = function(path) {
2
return Module._load(path, this);
3
}

由此可知,require 并不是全局性命令,而是每个模块提供的一个内部方法,也就是说,只有在模块内部才能使用 require 命令(唯一的例外是 REPL 环境)。另外,require 其实内部调用 Module._load 方法。接下来是 Module._load 的源码:

1
Module._load = function(request, parent, isMain) {
2
3
// 计算绝对路径
4
var filename = Module._resolveFilename(request, parent);
5
6
// 第一步:如果有缓存,取出缓存
7
var cachedModule = Module._cache[filename];
8
if (cachedModule) {
9
return cachedModule.exports;
23 collapsed lines
10
11
// 第二步:是否为内置模块
12
if (NativeModule.exists(filename)) {
13
return NativeModule.require(filename);
14
}
15
16
// 第三步:生成模块实例,存入缓存
17
var module = new Module(filename, parent);
18
Module._cache[filename] = module;
19
20
// 第四步:加载模块
21
try {
22
module.load(filename);
23
hadException = false;
24
} finally {
25
if (hadException) {
26
delete Module._cache[filename];
27
}
28
}
29
30
// 第五步:输出模块的exports属性
31
return module.exports;
32
}

上面代码中,首先解析出模块的绝对路径(filename),以它作为模块的识别符。然后,如果模块已经在缓存中,就从缓存取出;如果不在缓存中,就加载模块。所以 Module._load 的关键步骤是两个:

  1. Module._resolveFilename():确定模块的绝对路径
  2. module.load():加载模块

load

确定了模块的绝对路径以后,就可以开始加载模块了。以下是 Module.load 方法的源码:

1
Module.prototype.load = function(filename) {
2
var extension = path.extname(filename) || '.js';
3
if (!Module._extensions[extension]) extension = '.js';
4
Module._extensions[extension](this, filename);
5
this.loaded = true;
6
}

上面的实例代码中,首先确定了模块的后缀名,不同的后缀对应不同的加载方法。

1
Module._extensions['.js'] = function(module, filename) {
2
// 将模块文件读取成字符串
3
var content = fs.readFileSync(filename, 'utf8');
4
// 然后剥离 utf8 编码特有的BOM文件头,最后编译该模块。
5
module._compile(stripBOM(content), filename);
6
}
7
8
Module._extensions['.json'] = function(module, filename) {
9
var content = fs.readFileSync(filename, 'utf8');
7 collapsed lines
10
try {
11
module.exports = JSON.parse(stripBOM(content));
12
} catch (err) {
13
err.message = filename + ': ' + err.message;
14
throw err;
15
}
16
}

_compile

1
Module.prototype._compile = function(content, filename) {
2
var self = this;
3
var args = [self.exports, require, self, filename, dirname];
4
return compiledWrapper.apply(self.exports, args);
5
}

代码等同于:

1
(function (exports, require, module, __filename, __dirname) {
2
// do something
3
})

也就是说,模块的加载实质上就是,注入exports、require、module三个全局变量,然后执行模块的源码,然后将模块的 exports 变量的值输出。那么可以根据编译的过程得出 require 方法加载模块时调用的钩子函数。

  1. _load
  2. load
  3. _extensions
  4. _compile

从上面的执行流程可以知道,在引入了模块以后,是在 _extensions 方法内读取的文件内容,那么在这里面做一点点操作,那么就可以达到修改模块引入结果的效果,就像 ts-node 一样。

1
const Module = require('module');
2
const fs = require('fs');
3
4
Module._extensions['.js'] = function (module, filename) {
5
let content = fs.readFileSync(filename, 'utf8');
6
7
// 你可以只针对包含特定文件名的文件进行更改
8
if (filename.includes('input')) {
9
// 修改返回的内容
4 collapsed lines
10
content = content.replace('```code', '```demo');
11
}
12
module._compile(content, filename);
13
}

  1. require() 源码解读
  2. 给大家变个 Node.js 的小魔术

CD ..
接下来阅读
HTML Element