ToC
usage
在开始之前,可以先看一下基本用法。
1// 前提代码2const resolved = require(X);3const Module = require('module');
- 如果 X 是内置模块则直接返回模块。
- 内置模块可以通过 module.builtinModules 属性来查看。
1 console.log(Module.builtinModules);
如果 X 模块路径是以
/
、./
、../
开头- 根据 X 所在的父模块,确定 X 的绝对路径。
- 将 X 当成文件,依次查找下面文件,只要有一个存在,则直接返回文件不再查找,可以通过 module._extensions 属性查看受支持的文件类型列表
- X.js
- X.json
- X.node
- 将 X 当成目录,依次查找下面文件。只要有一个存在就返回,不再继续执行
- X/package.json(main字段)
- X/index.js
- X/index.json
- X/index.node
如果 X 不带路径,比如
''
、'.'
、'..'
等。- 根据 X 所在的父模块,确定 X 可能的安装目录。
- 依次在每个目录中,将 X 当成文件名或目录名加载。
throw module not found.
require
每个实例都有一个 require
方法。
1Module.prototype.require = function(path) {2 return Module._load(path, this);3}
由此可知,require 并不是全局性命令,而是每个模块提供的一个内部方法,也就是说,只有在模块内部才能使用 require 命令(唯一的例外是 REPL 环境)。另外,require 其实内部调用 Module._load 方法。接下来是 Module._load
的源码:
1Module._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;10
22 collapsed lines
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
的关键步骤是两个:
- Module._resolveFilename():确定模块的绝对路径
- module.load():加载模块
load
确定了模块的绝对路径以后,就可以开始加载模块了。以下是 Module.load
方法的源码:
1Module.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}
上面的实例代码中,首先确定了模块的后缀名,不同的后缀对应不同的加载方法。
1Module._extensions['.js'] = function(module, filename) {2 // 将模块文件读取成字符串3 var content = fs.readFileSync(filename, 'utf8');4 // 然后剥离 utf8 编码特有的BOM文件头,最后编译该模块。5 module._compile(stripBOM(content), filename);6}7
8Module._extensions['.json'] = function(module, filename) {9 var content = fs.readFileSync(filename, 'utf8');10 try {6 collapsed lines
11 module.exports = JSON.parse(stripBOM(content));12 } catch (err) {13 err.message = filename + ': ' + err.message;14 throw err;15 }16}
_compile
1Module.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 something3})
也就是说,模块的加载实质上就是,注入exports、require、module三个全局变量,然后执行模块的源码,然后将模块的 exports 变量的值输出。那么可以根据编译的过程得出 require
方法加载模块时调用的钩子函数。
- _load
- load
- _extensions
- _compile
从上面的执行流程可以知道,在引入了模块以后,是在 _extensions
方法内读取的文件内容,那么在这里面做一点点操作,那么就可以达到修改模块引入结果的效果,就像 ts-node
一样。
1const Module = require('module');2const fs = require('fs');3
4Module._extensions['.js'] = function (module, filename) {5 let content = fs.readFileSync(filename, 'utf8');6
7 // 你可以只针对包含特定文件名的文件进行更改8 if (filename.includes('input')) {9 // 修改返回的内容10 content = content.replace('```code', '```demo');3 collapsed lines
11 }12 module._compile(content, filename);13}