Node.js的require Hook 魔术

2022.9.5 星期一

# Node.js 的 require hook 的魔术

魔术揭秘

Node.js 加载模块的流程是这样的:
模块加载会调用 load 方法, load 会调用对应后缀名的 _extensions 的方法来处理,其中会调用 _compile 来编译并把结果放入 cache,之后返回。
所以呢?我们想改变 js 模块的返回值,只需要改造下 Module._extensions[‘.js’] 就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// input.js
function func() {
return '卡颂'
}
module.exports = func();


// test.js // 在另一个模块 test.js 中引入这个 input.js,然后打印一下:
const data = require('./input.js');
console.log(data);


// entry.js // 在 entry.js 里面引入 test.js:
require('./test.js');

我们想改变 js 模块的返回值,只需要改造下 Module._extensions[‘.js’] 就可以了。

1
2
3
4
5
6
7
8
9
10
const Module = require('module');
const fs = require('fs');

Module._extensions['.js'] = function (module, filename) {
let content = fs.readFileSync(filename, 'utf8');
if (filename.includes('input')) {
content = content.replace('卡颂', '卡帅');
}
module._compile(content, filename);
};

应用场景

比如说 ts-node,它是怎么做到直接 require ts 模块的?就是通过 require hook 偷偷做了编译,其实你执行的是编译后的 js。
比如说 babel-register 它是怎么做到直接执行带有 esnext 新特性的代码的?也是通过 require hook 偷偷做了编译。
还有覆盖率测试,其实是通过函数插桩做到的,也就是你每执行一条语句都会计数。怎么插桩呢?跑单测的时候也没手动插桩啊,就是因为工具内部偷偷通过 require hook 做了插桩,才能得到覆盖率数据。
<!– 东东: 这个魔术还挺有用的嘛。学会了~

总结

Node.js 的 js 模块加载的流程是 load -> _extensions[‘.js’] -> _compile,可以通过修改 _extensions[‘.js’] 来达到 hook 的目的,比如在 _compile 之前做一些代码转换。
这种 hook 在 babel-register、ts-node 还有单测的覆盖率测试中都有应用,能够达到透明的修改代码的目的。
因为开发者不知道代码什么时候被修改的,所以看起来比较神奇
–>

knowledge is no pay,reward is kindness
0%