手寫一個 ts-node 來深入理解它的原理
當我們用 Typesript 來寫 Node.js 的代碼,寫完代碼之後要用 tsc 作編譯,之後再用 Node.js 來跑,這樣比較麻煩,所以我們會用 ts-node 來直接跑 ts 代碼,省去了編譯階段。
有沒有覺得很神奇,ts-node 怎麼做到的直接跑 ts 代碼的?
其實原理並不難,今天我們來實現一個 ts-node 吧。
相關基礎
實現 ts-node 需要 3 方面的基礎知識:
-
require hook
-
repl 模塊、vm 模塊
-
ts compiler api
我們先學下這些基礎
require hook
Node.js 當 require 一個 js 模塊的時候,內部會分別調用 Module.load、 Module._extensions['.js'],Module._compile 這三個方法,然後纔是執行。
同理,ts 模塊、json 模塊等也是一樣的流程,那麼我們只需要修改 Module._extensions[擴展名] 的方法,就能達到 hook 的目的:
require.extensions['.ts'] = function(module, filename) {
// 修改代碼
module._compile(修改後的代碼, filename);
}
比如上面我們註冊了 ts 的處理函數,這樣當處理 ts 模塊時就會調用這個方法,所以我們在這裏面做編譯就可以了,這就是 ts-node 能夠直接執行 ts 的原理。
repl 模塊
Node.js 提供了 repl 模塊可以創建 Read、Evaluate、Print、Loop 的命令行交互環境,就是那種一問一答的方式。ts-node 也支持 repl 的模式,可以直接寫 ts 代碼然後執行,原理就是基於 repl 模塊做的擴展。
repl 的 api 是這樣的:通過 start 方法來創建一個 repl 的交互,可以指定提示符 prompt,可以自己實現 eval 的處理邏輯:
const repl = require('repl');
const r = repl.start({
prompt: '- . - > ',
eval: myEval
});
function myEval(cmd, context, filename, callback) {
// 對輸入的命令做處理
callback(null, 處理後的內容);
}
repl 的執行時有一個上下文的,在這裏就是 r.context,我們在這個上下文裏執行代碼要使用 vm 模塊:
const vm = require('vm');
const res = vm.runInContext(要執行的代碼, r.context);
這兩個模塊結合,就可以實現一問一答的命令行交互,而且 ts 的編譯也可以放在 eval 的時候做,這樣就實現了直接執行 ts 代碼。
ts compiler api
ts 的編譯我們主要是使用 tsc 的命令行工具,但其實它同樣也提供了編譯的 api,叫做 ts compiler api。我們做工具的時候就需要直接調用 compiler api 來做編譯。
轉換 ts 代碼爲 js 代碼的 api 是這個:
const { outputText } = ts.transpileModule(ts代碼, {
compilerOptions: {
strict: false,
sourceMap: false,
// 其他編譯選項
}
});
當然,ts 也提供了類型檢查的 api,因爲參數比較多,我們後面一篇文章再做展開,這裏只瞭解 transpileModule 的 api 就夠了。
瞭解了 require hook、repl 和 vm、ts compiler api 這三方面的知識之後,ts-node 的實現原理就呼之欲出了,接下來我們就來實現一下。
實現 ts-node
直接執行的模式
我們可以使用 ts-node + 某個 ts 文件,來直接執行這個 ts 文件,它的原理就是修改了 require hook,也就是 Module._extensions['.ts']
來實現的。
在 require hook 裏面做 ts 的編譯,然後後面直接執行編譯後的 js,這樣就能達到直接執行 ts 文件的效果。
所以我們重寫 Module._extensions['.ts']
方法,在裏面讀取文件內容,然後調用 ts.transpileModule 來把 ts 轉成 js,之後調用 Module._compile 來處理編譯後的 js。
這樣,我們就可以直接執行 ts 模塊了,具體的模塊路徑是通過命令行參數執行的,可以用 process.argv 來取。
const path = require('path');
const ts = require('typescript');
const fs = require('fs');
const filePath = process.argv[2];
require.extensions['.ts'] = function(module, filename) {
const fileFullPath = path.resolve(__dirname, filename);
const content = fs.readFileSync(fileFullPath, 'utf-8');
const { outputText } = ts.transpileModule(content, {
compilerOptions: require('./tsconfig.json')
});
module._compile(outputText, filename);
}
require(filePath);
我們準備一個這樣的 ts 文件 test.ts:
const a = 1;
const b = 2;
function add(a: number, b: number): number {
return a + b;
}
console.log(add(a, b));
然後用這個工具 hook.js 來跑:
可以看到,成功的執行了 ts,這就是 ts-node 的原理。
當然,細節的邏輯還有很多,但是最主要的原理就是 require hook + ts compiler api。
repl 模式
ts-node 支持啓動一個 repl 的環境,交互式的輸入 ts 代碼然後執行,它的原理就是基於 Node.js 提供的 repl 模塊做的擴展,在自定義的 eval 函數里面做了 ts 的編譯,然後使用 vm.runInContext 的 api 在 repl 的上下文中執行 js 代碼。
我們也啓動一個 repl 的環境,設置提示符和自定義的 eval 實現。
const repl = require('repl');
const r = repl.start({
prompt: '- . - > ',
eval: myEval
});
function myEval(cmd, context, filename, callback) {
}
eval 的實現就是編譯 ts 代碼爲 js,然後用 vm.runInContext 來執行編譯後的 js 代碼,執行的 context 指定爲 repl 的 context:
function myEval(cmd, context, filename, callback) {
const { outputText } = ts.transpileModule(cmd, {
compilerOptions: {
strict: false,
sourceMap: false
}
});
const res = vm.runInContext(outputText, r.context);
callback(null, res);
}
同時,我們還可以對 repl 的 context 做一些擴展,比如注入一個 who 的環境變量:
Object.defineProperty(r.context, 'who', {
configurable: false,
enumerable: true,
value: '神說要有光'
});
我們來測試下效果:
可以看到,執行後啓動了一個 repl 環境,提示符修改成了 -.- >,可以直接執行 ts 代碼,還可以訪問全局變量 who。
這就是 ts-node 的 repl 模式的大概原理:repl + vm + ts compiler api。
全部代碼如下:
const repl = require('repl');
const ts = require('typescript');
const vm = require('vm');
const r = repl.start({
prompt: '- . - > ',
eval: myEval
});
Object.defineProperty(r.context, 'who', {
configurable: false,
enumerable: true,
value: '神說要有光'
});
function myEval(cmd, context, filename, callback) {
const { outputText } = ts.transpileModule(cmd, {
compilerOptions: {
strict: false,
sourceMap: false
}
});
const res = vm.runInContext(outputText, r.context);
callback(null, res);
}
總結
ts-node 可以直接執行 ts 代碼,不需要手動編譯,爲了深入理解它,我們我們實現了一個簡易 ts-node,支持了直接執行和 repl 模式。
直接執行的原理是通過 require hook,也就是 Module._extensions[ext] 裏通過 ts compiler api 對代碼做轉換,之後再執行,這樣的效果就是可以直接執行 ts 代碼。
repl 的原理是基於 Node.js 的 repl 模塊做的擴展,可以定製提示符、上下文、eval 邏輯等,我們在 eval 裏用 ts compiler api 做了編譯,然後通過 vm.runInContext 在 repl 的 context 中執行編譯後的 js。這樣的效果就是可以在 repl 裏直接執行 ts 代碼。
當然,完整的 ts-node 還有很多細節,但是大概的原理我們已經懂了,而且還學到了 require hook、repl 和 vm 模塊、 ts compiler api 等知識。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/FlzXxqNNcMQqEleVkggtqA