給大家變個 Node.js 的小魔術
本文轉載自微信公眾號「神光的編程秘籍」,作者神說要有光。轉載本文請聯系神光的編程秘籍公眾號。
魔術演出
我們準備一個 Node.js 的模塊 input.js:
- // input.js
- function func() {
- return '卡頌'
- }
- module.exports = func();
這個模塊返回的值是啥?
東東:是“卡頌”。
那我在另一個模塊 test.js 中引入這個 input.js,然后打印一下:
- // test.js
- const data = require('./input.js');
- console.log(data);
之后我在 entry.js 里面引入 test.js:
- require('./test.js');
執行之后打印的是啥?
東東:是“卡頌”。
真的么?那我們跑一下:
打印的是啥:
東東:是 “卡帥”,哇,好神奇,怎么做到的。
我:想不想學?
東東:想。
我:那接下來就進入魔術揭秘時間。
魔術揭秘
Node.js 加載模塊的流程是這樣的:
模塊加載會調用 load 方法, load 會調用對應后綴名的 _extensions 的方法來處理,其中會調用 _compile 來編譯并把結果放入 cache,之后返回。
所以呢?我們想改變 js 模塊的返回值,只需要改造下 Module._extensions['.js'] 就可以了。
- 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);
- };
我們對 filename 為 input 的文件,讀取內容之后進行了替換,之后再調用 module._compile 來編譯,后續流程不變。
模塊引入方式不變,但是模塊內容已經悄悄的被修改了,這個魔術的名字叫做 require hook。
東東:原來是你藏了一段代碼沒展示。
我:魔術都是這樣的啊。而且你別小看了這個 require hook,它能做到很多強大的功能呢。
東東:哦?比如說
我:比如說 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 還有單測的覆蓋率測試中都有應用,能夠達到透明的修改代碼的目的。
因為開發者不知道代碼什么時候被修改的,所以看起來比較神奇。