当前位置: 首页 > 科技观察

手写一个Ts-Node,看懂它的原理

时间:2023-03-18 14:39:45 科技观察

本文转载自微信公众号《神光的编程秘籍》,作者神说必有光zxg。转载本文请联系神光编程秘籍公众号。我们在使用Typesript写Node.js代码的时候,写完代码需要用tsc编译,然后用Node.js运行。这样比较麻烦,所以我们会使用ts-node直接运行ts代码,省去编译阶段。ts-node可以直接运行ts代码,有没有觉得很神奇?其实原理并不难。今天来实现一个ts-node。相关基础ts-node的实现需要三个基础知识:requirehookreplmodule,vmmoduletscompilerapi先学习这些基础知识requirehookNode.jsrequire一个js模块时,会在内部调用Module.load和Module._extensions['.js'],Module._compile这三个方法,然后执行。同理,ts模块、json模块等也是同一个流程,那么我们只需要修改Module._extensions[扩展名]这个方法就可以达到hook的目的:require.extensions['.ts']=function(module,filename){//修改代码module._compile(modifiedcode,filename);}比如我们上面注册了ts处理函数,这样在处理ts模块的时候就会调用这个方法,所以我们编译到这里就OK了,这就是ts-node可以直接执行ts的原理。repl模块Node.js提供了一个repl模块,可以创建Read、Evaluate、Print和Loop的命令行交互环境,也就是提问和回答的方式。ts-node也支持repl模式,可以直接写ts代码然后执行,原理是基于repl模块做的扩展。repl的api是这样的:通过start方法创建repl交互,可以指定prompt提示,eval处理逻辑可以自己实现:constrepl=require('repl');constr=repl.start({prompt:'-.->',eval:myEval});functionmyEval(cmd,context,filename,callback){//处理输入的命令callback(null,processedcontent);}repl执行时有上下文,这里是r.context,我们需要使用vm模块来执行这个上下文中的代码:constvm=require('vm');constres=vm.runInContext(要执行的代码,r.context);这两个模块的结合,可以实现一问一答的命令行交互,并且在eval的时候也可以进行ts的编译,从而实现ts代码的直接执行。我们主要使用tsc命令行工具来编译ts编译器api,但实际上它也提供了一个编译好的api,叫做tscompilerapi。我们在制作工具的时候,需要直接调用编译器api进行编译。ts代码转js代码的api是这样的:const{outputText}=ts.transpileModule(tscode,{compilerOptions:{strict:false,sourceMap:false,//其他编译选项}});当然ts也提供了类型检查的api,因为参数比较多,我们会在后面的文章中展开。这里,只了解transpileModule的api就够了。了解了requirehook、repl和vm、ts编译api这三个方面之后,ts-node的实现原理就呼之欲出了,下面我们来实现一下。要实现ts-node的直接执行模式,我们可以使用ts-node+一个ts文件,直接执行这个ts文件。它的原理是修改requirehook,也就是Module._extensions['.ts']来实现。在requirehook中做ts编译,后面直接执行编译好的js,这样就可以达到直接执行ts文件的效果。于是我们重写了Module._extensions['.ts']方法,读取里面的文件内容,然后调用ts.transpileModule把ts转成js,然后调用Module._compile处理编译好的js。这样我们就可以直接执行ts模块了,具体的模块路径是通过命令行参数执行的,可以通过process.argv获取。constpath=require('path');constts=require('typescript');constfs=require('fs');constfilePath=process.argv[2];require.extensions['.ts']=function(模块,文件名){constfileFullPath=path.resolve(__dirname,filename);constcontent=fs.readFileSync(fileFullPath,'utf-8');const{outputText}=ts.transpileModule(content,{compilerOptions:require('./tsconfig.json')});module._compile(outputText,filename);}require(filePath);我们准备了这样一个ts文件test.ts:consta=1;constb=2;functionadd(a:number,b:number):number{returna+b;}console.log(add(a,b));然后使用这个工具hook.js运行:可以看到ts执行成功,这就是ts-node的原理。当然还有很多更详细的逻辑,但主要原理是requirehook+tscompilerapi。repl模式ts-node支持启动一个repl环境,交互输入ts代码然后执行。它的原理是基于Node.js提供的repl模块的扩展,在自定义的eval函数中编译ts。然后使用vm.runInContext的api在repl的上下文中执行js代码。我们还启动了一个repl环境,设置了一个提示符和一个自定义eval实现。constrepl=require('repl');constr=repl.start({prompt:'-.->',eval:myEval});functionmyEval(cmd,context,filename,callback){}eval的实现是编译ts代码js,然后使用vm.runInContext执行编译后的js代码,执行上下文指定为repl上下文:functionmyEval(cmd,context,filename,callback){const{outputText}=ts.transpileModule(cmd,{compilerOptions:{strict:false,sourceMap:false}});constres=vm.runInContext(outputText,r.context);callback(null,res);}同时我们还可以对context做一些扩展repl,比如注入一个who环境变量:Object.defineProperty(r.context,'who',{configurable:false,enumerable:true,value:'神说必有光'});我们来测试一下效果:可以看到执行后开始创建了一个repl环境,提示符改为-.->,可以直接执行ts代码,可以访问全局变量who。这是ts-node的repl模式的大致原理:repl+vm+tscompilerapi。整个代码如下:constrepl=require('repl');constts=require('typescript');constvm=require('vm');constr=repl.start({prompt:'-.->',eval:myEval});Object.defineProperty(r.context,'who',{configurable:false,enumerable:true,value:'上帝说要有光'});functionmyEval(cmd,context,filename,callback){const{outputText}=ts.transpileModule(cmd,{compilerOptions:{strict:false,sourceMap:false}});constres=vm.runInContext(outputText,r.context);callback(null,res);}总结ts-node可以直接执行ts代码,不需要手动编译。为了深入理解它,我们实现了一个简单的支持直接执行和repl模式的ts-node。直接执行的原理是通过requirehook,即通过Module._extensions[ext]中的ts编译器api,将代码进行转换,然后执行。这样做的效果是可以直接执行ts代码。repl的原理是基于Node.js的repl模块的扩展,可以自定义提示符、上下文、eval逻辑等,我们在eval中使用ts编译器api进行编译,然后在上下文中执行编译repl的通过vm.runInContext后js。这样做的效果是可以直接在repl中执行ts代码。当然,完整的ts-node还有很多细节,但是我们已经了解了大概的原理,也学习了requirehook、repl和vm模块、ts编译api等知识。