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


















 
 
 












 
 
 
 