手写一个 Ts-Node 来深入理解它的原理

开发 前端
当我们用 Typesript 来写 Node.js 的代码,写完代码之后要用 tsc 作编译,之后再用 Node.js 来跑,这样比较麻烦,所以我们会用 ts-node 来直接跑 ts 代码,省去了编译阶段。

[[437960]]

本文转载自微信公众号「神光的编程秘籍」,作者神说要有光zxg。转载本文请联系神光的编程秘籍公众号。

当我们用 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 的目的:

  1. require.extensions['.ts'] = function(module, filename) { 
  2.  
  3. // 修改代码 
  4.  
  5. module._compile(修改后的代码, filename); 
  6.  

比如上面我们注册了 ts 的处理函数,这样当处理 ts 模块时就会调用这个方法,所以我们在这里面做编译就可以了,这就是 ts-node 能够直接执行 ts 的原理。

repl 模块

Node.js 提供了 repl 模块可以创建 Read、Evaluate、Print、Loop 的命令行交互环境,就是那种一问一答的方式。ts-node 也支持 repl 的模式,可以直接写 ts 代码然后执行,原理就是基于 repl 模块做的扩展。

repl 的 api 是这样的:通过 start 方法来创建一个 repl 的交互,可以指定提示符 prompt,可以自己实现 eval 的处理逻辑:

  1. const repl = require('repl'); 
  2.  
  3. const r = repl.start({  
  4.     prompt: '- . - > ',  
  5.     eval: myEval  
  6. }); 
  7.  
  8. function myEval(cmd, context, filename, callback) { 
  9.     // 对输入的命令做处理 
  10.     callback(null, 处理后的内容); 

repl 的执行时有一个上下文的,在这里就是 r.context,我们在这个上下文里执行代码要使用 vm 模块:

  1. const vm = require('vm'); 
  2.  
  3. const res = vm.runInContext(要执行的代码, r.context); 

这两个模块结合,就可以实现一问一答的命令行交互,而且 ts 的编译也可以放在 eval 的时候做,这样就实现了直接执行 ts 代码。

ts compiler api

ts 的编译我们主要是使用 tsc 的命令行工具,但其实它同样也提供了编译的 api,叫做 ts compiler api。我们做工具的时候就需要直接调用 compiler api 来做编译。

转换 ts 代码为 js 代码的 api 是这个:

  1. const { outputText } = ts.transpileModule(ts代码, { 
  2.     compilerOptions: { 
  3.         strict: false
  4.         sourceMap: false
  5.         // 其他编译选项 
  6.     } 
  7. }); 

当然,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 来取。

  1. const path = require('path'); 
  2. const ts = require('typescript'); 
  3. const fs = require('fs'); 
  4.  
  5. const filePath = process.argv[2]; 
  6.  
  7. require.extensions['.ts'] = function(module, filename) { 
  8.     const fileFullPath = path.resolve(__dirname, filename); 
  9.     const content = fs.readFileSync(fileFullPath, 'utf-8'); 
  10.  
  11.     const { outputText } = ts.transpileModule(content, { 
  12.         compilerOptions: require('./tsconfig.json'
  13.     }); 
  14.  
  15.     module._compile(outputText, filename); 
  16.  
  17. require(filePath); 

我们准备一个这样的 ts 文件 test.ts:

  1. const a = 1; 
  2. const b = 2; 
  3.  
  4. function add(a: number, b: number): number { 
  5.     return a + b; 
  6.  
  7. 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 实现。

  1. const repl = require('repl'); 
  2.  
  3. const r = repl.start({  
  4.     prompt: '- . - > ',  
  5.     eval: myEval  
  6. }); 
  7.  
  8. function myEval(cmd, context, filename, callback) { 
  9.  

eval 的实现就是编译 ts 代码为 js,然后用 vm.runInContext 来执行编译后的 js 代码,执行的 context 指定为 repl 的 context:

  1. function myEval(cmd, context, filename, callback) { 
  2.     const { outputText } = ts.transpileModule(cmd, { 
  3.         compilerOptions: { 
  4.             strict: false
  5.             sourceMap: false 
  6.         } 
  7.     }); 
  8.     const res = vm.runInContext(outputText, r.context); 
  9.     callback(null, res); 

同时,我们还可以对 repl 的 context 做一些扩展,比如注入一个 who 的环境变量:

  1. Object.defineProperty(r.context, 'who', { 
  2.   configurable: false
  3.   enumerable: true
  4.   value: '神说要有光' 
  5. }); 

我们来测试下效果:

可以看到,执行后启动了一个 repl 环境,提示符修改成了 -.- >,可以直接执行 ts 代码,还可以访问全局变量 who。

这就是 ts-node 的 repl 模式的大概原理:repl + vm + ts compiler api。

全部代码如下:

  1. const repl = require('repl'); 
  2. const ts = require('typescript'); 
  3. const vm = require('vm'); 
  4.  
  5. const r = repl.start({  
  6.     prompt: '- . - > ',  
  7.     eval: myEval  
  8. }); 
  9.  
  10. Object.defineProperty(r.context, 'who', { 
  11.   configurable: false
  12.   enumerable: true
  13.   value: '神说要有光' 
  14. }); 
  15.  
  16. function myEval(cmd, context, filename, callback) { 
  17.     const { outputText } = ts.transpileModule(cmd, { 
  18.         compilerOptions: { 
  19.             strict: false
  20.             sourceMap: false 
  21.         } 
  22.     }); 
  23.     const res = vm.runInContext(outputText, r.context); 
  24.     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 等知识。

 

责任编辑:武晓燕 来源: 神光的编程秘籍
相关推荐

2022-04-26 08:32:36

CSS前端

2020-07-03 17:20:07

Redux前端代码

2021-10-16 05:00:32

.js Buffer模块

2021-08-05 05:46:06

Node.jsInspector工具

2022-01-14 12:28:18

架构OpenFeign远程

2020-08-10 18:03:54

Cache存储器CPU

2024-04-15 00:00:00

技术Attention架构

2021-03-10 10:55:51

SpringJava代码

2022-11-04 09:43:05

Java线程

2024-03-12 00:00:00

Sora技术数据

2022-09-05 08:39:04

kubernetesk8s

2021-08-12 01:00:29

NodejsAsync

2021-08-26 13:57:56

Node.jsEncodingBuffer

2020-03-17 08:36:22

数据库存储Mysql

2019-07-01 13:34:22

vue系统数据

2020-11-04 15:35:13

Golang内存程序员

2022-09-05 22:22:00

Stream操作对象

2023-10-13 13:30:00

MySQL锁机制

2023-09-19 22:47:39

Java内存

2022-09-26 08:01:31

线程LIFO操作方式
点赞
收藏

51CTO技术栈公众号