NodeJs中实现require拦截的几种方法
最近的几个开发中,需要对 require 进行拦截,阻止不符合规范的 require ,比如禁止引用一些指定的模块,进制引用某个目录下的模块等,实现 require 的拦截,大体上有三种方法,下面一一介绍。
拦截模块引用
在一些特殊需求的场景下,需要对 require 的模块进行记录或者拦截,场景之一就是进制引用一些指定的模块,或者进制引用某个目录下的模块,这时就需要对 require 进行监听和拦截,以便能及时发现这些非法引用。
拦截 require 首先能想到的就是重写 require 方法,但重写 require 方法只能在被重新的文件内有效,无法在项目内生效,即便如此,对于拦截 require ,三种方案如下:
- 重写 require 方法;
- 使用 vm 加载模块的同时注入重写的 require 方法;
- 使用 Module ,重写 _load 方法;
其中直接重写 require 有局限性,使用 vm 可以解决重写 require 的局限性,但在文件模块较多时会出现性能问题,重写 model._load 方案最为合理,下面一一介绍并实现。
重写 require 方法
这是最容易想到的一种防范,假如有一个 math.js
模块如下:
exports.sum = (nums = []) => {
return nums.reduce((prev, curt) => {
return prev + curt;
}, 0);
}
另一个文件中 index.js 中引用该模块,并计算给定值:
const { sum } = require('./math');
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(sum(numbers));
显而易见的,上面的代码会输出 55 ,这是一个求个函数。
那么如果要在 index.js
中拦截 require ,我们通过重新 require 便可以实现:
// 保存原 require 方法,并重新定义 require 方法
const modelRequire = require;
require = (...args) => {
// 拦截参数
console.log('params is: ', args);
return modelRequire(...args);
}
const { sum } = require('./math');
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(sum(numbers));
执行上面的代码会输出:
params is: [ './math' ]
55
上面的代码通过重写 require 实现了引入拦截,但是局限性却很大,假如 math.js
中又 require 了其他模块,那么重写在 index.js 中的 require 将无能为力。
使用 vm 加载模块
使用 vm 模块可以解决上面的局限性,其思路是将入口文件以外的其他所有文件,都放到虚拟环境中执行,在虚拟环境中注入重写的 require 方法,这样便可在后续的模块引用中实现拦截。
function requireVmModule(fileContext) {
const injectModule = { module };
const sandbox = vm.createContext(injectModule);
return vm.runInContext(fileContext, sandbox);
}
/**
* 以VM的方式加载模块
* @param {String} modelName 模块名
*/
function loadModule(modelName) {
// 依赖,直接 require
if (!/\.js/.test(modelName) && !/^\.\//.test(modelName)) {
return requireVmModule(modelName);
}
const relativeModuleName = path.resolve(__dirname, modelName);
const moduleJsFile = relativeModuleName + '.js';
if (fs.existsSync(moduleJsFile)) {
return requireVmModule(fs.readFileSync(moduleJsFile));
}
if (fs.existsSync(relativeModuleName)) {
return requireVmModule(fs.readFileSync(relativeModuleName));
}
const moduleIndexFile = path.join(relativeModuleName, 'index.js');
if (fs.existsSync(moduleIndexFile)) {
return requireVmModule(fs.readFileSync(moduleIndexFile));
}
throw new Error(`Module{modelName} not found`);
}
在项目中尝试使用 loadModule 代替 require :
// 重写当前的 require
require = loadModule;
const { sum, getTypeOf } = require('./math.js');
console.log(sum([1, 2, 3]));
// 输出 6
由此可见使用 vm 的方式也可以实现文件的加载拦截,其实现方式也是重写 require ,只不过使用的是 vm 模块来载入程序。
处理递归引入
上面的代码并未处理递归引入的问题,即当子模块内部继续引入其他模块时,我们也需要对子模块的引入进行拦截。也很简单,由于引入模块始终会使用 require 来导入,所以我们再去监听子模块的 require 即可,在子模块执行 require 的时候继续创建虚拟环境,注入重写的 require 方法即可。
修改 requireVmModule 方法如下:
function requireVmModule(fileContext) {
const injectModule = {
module,
// 重写 require
require(moduleName) {
return loadModule(moduleName);
}
};
const sandbox = vm.createContext(injectModule);
return vm.runInContext(fileContext, sandbox);
}
上面的代码中,当被引入的模块中继续引入了其他模块时,其内部就会调用我们注入的 require
方法,这个方法内部又继续调用了 loadModule 方法,所以便实现了递归引入拦截。
重写 model._load 方法
如果使用 Module 的 _load 方法来实现,那么则更简单且更可靠。
const { Module } = require('module');
const loadBackend = Module._load;
Module._load = (request, parent, isMain) => {
// 处理 require 拦截
console.log(request, parent, isMain);
return loadBackend(request, parent, isMain);
}
const { sum } = require('./math');
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(sum(numbers));
如上代码所示,重写 _load 方法必须在 require 之前,通过重写,之后所有 require 的模块都可以被拦截到。
与使用 vm 的方式相比,这种方案效率更高,模块的加载速度更快。
虽然上面的代码实现了 require 拦截,但还不够优美,上面定义是一次性的,当重写 _load 以后,后续的所有 require 都将被拦截,如果有些部分的代码我们并不需要处理拦截规则,按照现在的方式会显得不那么方便处理。
对 _load 的改进
我们可以定义一个方法,通过闭包特性来启用 require 拦截,同时也支持关闭拦截,这样一样就可以在需要的时候开启它,在不需要的时候关闭它。
const { Module } = require('module');
/**
* 启用 require 拦截
* @returns
*/
function startRequirIntercept() {
const loadBackend = Module._load;
Module._load = (request, parent, isMain) => {
// 处理 require 拦截
console.log(request);
return loadBackend(request, parent, isMain);
}
return { resume() { Module._load = loadBackend } };
}
const intercept = startRequirIntercept();
const { sum } = require('./math');
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(sum(numbers));
// 恢复到初始状态
intercept.resume();
上面的代码实现了一个闭包函数,通过执行这个函数来启用拦截,闭包返回一个对象,通过对象的 resume
方法可以将 require 恢复到初始状态,这样一来就可以在需要 require 拦截的地方执行闭包函数,在拦截结束后恢复到初始状态。
小结
本文所介绍的是特殊需求的特殊处理,实际开发中很少会遇到需要拦截 require
的需求,不过借助对方案的探索和理解,可以加深对 nodejs 模块的认知,比如综上来看,使用 vm 虽然拥有更好的灵活性,但是在文件数目较多,引入较深的情况下会存在性能瓶颈,因为它需要频繁操作文件;而此时使用对 _load
重写的方式则更高效,实际开发中可酌情选择方案。