手寫一個 Ts-Node 來深入理解它的原理
本文轉(zhuǎn)載自微信公眾號「神光的編程秘籍」,作者神說要有光zxg。轉(zhuǎn)載本文請聯(lián)系神光的編程秘籍公眾號。
當我們用 Typesript 來寫 Node.js 的代碼,寫完代碼之后要用 tsc 作編譯,之后再用 Node.js 來跑,這樣比較麻煩,所以我們會用 ts-node 來直接跑 ts 代碼,省去了編譯階段。
有沒有覺得很神奇,ts-node 怎么做到的直接跑 ts 代碼的?
其實原理并不難,今天我們來實現(xiàn)一個 ts-node 吧。
相關(guān)基礎
實現(xiàn) ts-node 需要 3 方面的基礎知識:
- require hook
- repl 模塊、vm 模塊
- ts compiler api
我們先學下這些基礎
require hook
Node.js 當 require 一個 js 模塊的時候,內(nèi)部會分別調(diào)用 Module.load、 Module._extensions['.js'],Module._compile 這三個方法,然后才是執(zhí)行。
同理,ts 模塊、json 模塊等也是一樣的流程,那么我們只需要修改 Module._extensions[擴展名] 的方法,就能達到 hook 的目的:
- require.extensions['.ts'] = function(module, filename) {
- // 修改代碼
- module._compile(修改后的代碼, filename);
- }
比如上面我們注冊了 ts 的處理函數(shù),這樣當處理 ts 模塊時就會調(diào)用這個方法,所以我們在這里面做編譯就可以了,這就是 ts-node 能夠直接執(zhí)行 ts 的原理。
repl 模塊
Node.js 提供了 repl 模塊可以創(chuàng)建 Read、Evaluate、Print、Loop 的命令行交互環(huán)境,就是那種一問一答的方式。ts-node 也支持 repl 的模式,可以直接寫 ts 代碼然后執(zhí)行,原理就是基于 repl 模塊做的擴展。
repl 的 api 是這樣的:通過 start 方法來創(chuàng)建一個 repl 的交互,可以指定提示符 prompt,可以自己實現(xiàn) eval 的處理邏輯:
- const repl = require('repl');
- const r = repl.start({
- prompt: '- . - > ',
- eval: myEval
- });
- function myEval(cmd, context, filename, callback) {
- // 對輸入的命令做處理
- callback(null, 處理后的內(nèi)容);
- }
repl 的執(zhí)行時有一個上下文的,在這里就是 r.context,我們在這個上下文里執(zhí)行代碼要使用 vm 模塊:
- const vm = require('vm');
- const res = vm.runInContext(要執(zhí)行的代碼, r.context);
這兩個模塊結(jié)合,就可以實現(xiàn)一問一答的命令行交互,而且 ts 的編譯也可以放在 eval 的時候做,這樣就實現(xiàn)了直接執(zhí)行 ts 代碼。
ts compiler api
ts 的編譯我們主要是使用 tsc 的命令行工具,但其實它同樣也提供了編譯的 api,叫做 ts compiler api。我們做工具的時候就需要直接調(diào)用 compiler api 來做編譯。
轉(zhuǎn)換 ts 代碼為 js 代碼的 api 是這個:
- const { outputText } = ts.transpileModule(ts代碼, {
- compilerOptions: {
- strict: false,
- sourceMap: false,
- // 其他編譯選項
- }
- });
當然,ts 也提供了類型檢查的 api,因為參數(shù)比較多,我們后面一篇文章再做展開,這里只了解 transpileModule 的 api 就夠了。
了解了 require hook、repl 和 vm、ts compiler api 這三方面的知識之后,ts-node 的實現(xiàn)原理就呼之欲出了,接下來我們就來實現(xiàn)一下。
實現(xiàn) ts-node
直接執(zhí)行的模式
我們可以使用 ts-node + 某個 ts 文件,來直接執(zhí)行這個 ts 文件,它的原理就是修改了 require hook,也就是 Module._extensions['.ts'] 來實現(xiàn)的。
在 require hook 里面做 ts 的編譯,然后后面直接執(zhí)行編譯后的 js,這樣就能達到直接執(zhí)行 ts 文件的效果。
所以我們重寫 Module._extensions['.ts'] 方法,在里面讀取文件內(nèi)容,然后調(diào)用 ts.transpileModule 來把 ts 轉(zhuǎn)成 js,之后調(diào)用 Module._compile 來處理編譯后的 js。
這樣,我們就可以直接執(zhí)行 ts 模塊了,具體的模塊路徑是通過命令行參數(shù)執(zhí)行的,可以用 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 來跑:
可以看到,成功的執(zhí)行了 ts,這就是 ts-node 的原理。
當然,細節(jié)的邏輯還有很多,但是最主要的原理就是 require hook + ts compiler api。
repl 模式
ts-node 支持啟動一個 repl 的環(huán)境,交互式的輸入 ts 代碼然后執(zhí)行,它的原理就是基于 Node.js 提供的 repl 模塊做的擴展,在自定義的 eval 函數(shù)里面做了 ts 的編譯,然后使用 vm.runInContext 的 api 在 repl 的上下文中執(zhí)行 js 代碼。
我們也啟動一個 repl 的環(huán)境,設置提示符和自定義的 eval 實現(xiàn)。
- const repl = require('repl');
- const r = repl.start({
- prompt: '- . - > ',
- eval: myEval
- });
- function myEval(cmd, context, filename, callback) {
- }
eval 的實現(xiàn)就是編譯 ts 代碼為 js,然后用 vm.runInContext 來執(zhí)行編譯后的 js 代碼,執(zhí)行的 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 的環(huán)境變量:
- Object.defineProperty(r.context, 'who', {
- configurable: false,
- enumerable: true,
- value: '神說要有光'
- });
我們來測試下效果:
可以看到,執(zhí)行后啟動了一個 repl 環(huán)境,提示符修改成了 -.- >,可以直接執(zhí)行 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);
- }
總結(jié)
ts-node 可以直接執(zhí)行 ts 代碼,不需要手動編譯,為了深入理解它,我們我們實現(xiàn)了一個簡易 ts-node,支持了直接執(zhí)行和 repl 模式。
直接執(zhí)行的原理是通過 require hook,也就是 Module._extensions[ext] 里通過 ts compiler api 對代碼做轉(zhuǎn)換,之后再執(zhí)行,這樣的效果就是可以直接執(zhí)行 ts 代碼。
repl 的原理是基于 Node.js 的 repl 模塊做的擴展,可以定制提示符、上下文、eval 邏輯等,我們在 eval 里用 ts compiler api 做了編譯,然后通過 vm.runInContext 在 repl 的 context 中執(zhí)行編譯后的 js。這樣的效果就是可以在 repl 里直接執(zhí)行 ts 代碼。
當然,完整的 ts-node 還有很多細節(jié),但是大概的原理我們已經(jīng)懂了,而且還學到了 require hook、repl 和 vm 模塊、 ts compiler api 等知識。