Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

手写一个 ts-node 来深入理解它的原理 - IM Geek开发者社区-移动开发者社区-开源社区-IM Geek官网 #32

Open
zepang opened this issue Jan 20, 2022 · 0 comments

Comments

@zepang
Copy link
Owner

zepang commented Jan 20, 2022

当我们用 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 等知识。

题外话

其实 ts-node 的原理是应一个同学的要求写的,大家有想读的 nodejs 工具的源码也可以告诉我呀 (可以加我微信),无偿提供源码带读 + 简易实现的服务,不过会做一些筛选。

作者:zxg_神说要有光
来源:https://juejin.cn/post/7036688014206042143
https://www.imgeek.org/article/825359282

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant