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

又一款基于Esbuild的神器!

时间:2023-03-16 10:35:49 科技观察

Node.js不支持直接执行TS文件。如果我们要执行ts文件,可以使用ts-node库。相信有朋友在工作中也用过这个库。ts-node库的相关内容我就不介绍了,因为本文的主角是antfuboss开源的esno项目。接下来,小编带大家一探究竟。站起来揭开这个项目背后的秘密。看完这篇文章,你就会明白esno项目是如何执行ts文件的。此外,你将学习如何劫持Node.js的require函数,如何在ESModule的import语句中添加hooks,以及如何自定义httpsloader以支持importReactfrom"https://img.ydisp.cn/news/20220903/xehwysq3pjk.0”导入方法。什么是埃斯诺?esno是基于esbuild的TS/ESNext节点运行时。库会针对不同的模块化标准采用不同的解决方案:esno-CJS模式下的Node-byesbuild-registeresmo-ESM模式下的Node-byesbuild-node-loaderesno的使用方式很简单,可以global使用,也可以Local使用安装方式:全局安装$npmi-gesno安装成功后,可以通过以下方式直接执行ts文件:$esnoindex.ts$esmoindex.ts本地安装$npmiesno并forIn部分安装的话,一般情况下,我们会以npmscripts:{"scripts":{"start":"esnoindex.ts"},"dependencies":{"esno":"0.14.0"}}esno是如何工作的?在开始分析esno的工作原理之前,先来熟悉一下这个项目:├──LICENSE├──README.md├──esmo.mjs├──esno.js├──package.json├──pnpm-lock.yaml├──publish.ts└──tsconfig.json观察上面的项目结构,项目并不复杂。在项目根目录的package.json文件中,我们看到了前面介绍的esno和esmo命令。{"bin":{"esno":"esno.js","esmo":"esmo.mjs"},}此外,在package.json的脚本字段中,我们找到了发布命令。顾名思义,这个命令就是用来发布版本的。{"scripts":{"release":"npxbumpp--tag--commit--push&&nodeesmo.mjspublish.ts"},}需要注意的是,在publish.ts文件中,2021年是使用了Github上最耀眼的项目zx,有了它我们可以轻松编写命令行脚本。在撰写本文时,其Star数已达到27.5K。强烈建议感兴趣的朋友关注本项目。简单介绍完esno项目,我们来分析esno.js文件:#!/usr/bin/envnodeconstspawn=require('cross-spawn')constspawnSync=spawn.syncconstregister=require.resolve('esbuild-register')constargv=process.argv.slice(2)process.exit(spawnSync('node',['-r',register,...argv],{stdio:'inherit'}).status)从上面的代码可以看出,在执行esnoindex.ts命令时,会启动Node.js程序,通过spawnSync执行脚本。需要注意的是,执行时使用了-r选项,该选项的作用是预加载模块:-r,--require=...moduletopreload(option可以重复)这里预加载的模块是esbuild-register,这个模块是用esno命令执行ts文件的幕后功臣。什么是esbuild-register?esbuild-register是一个基于esbuild转换JSX、TS和esnext特性的工具。您可以通过多种方式安装它:$npmiesbuildesbuild-register-D#或Yarn$yarnaddesbuildesbuild-register--dev#或pnpm$pnpmaddesbuildesbuild-register-D安装成功后,您可以在命令行直接通过node应用程序执行ts文件:$node-resbuild-registerfile.ts-r,--require=...moduletopreload(option可以重复)-r用于指定预加载文件,即在执行file.ts文件之前,会提前加载esbuild-register模块,它会使用tsconfig.json中的jsxFactory、jsxFragmentFactory和target配置项进行转换操作。esbuild-register不仅可以在命令行上使用,还可以通过API使用:const{register}=require('esbuild-register/dist/node')const{unregister}=register({//...options})//如果你不再需要require钩子,取消注册unregister()了解了esbuild-register的基本用法后,我们来分析一下它内部是如何工作的。esbuild-register是如何工作的?esbuild-register在内部使用pirates库劫持了Node.js的require函数,可以直接在命令行执行ts文件。下面我们来看一下esbuild-register模块中定义的register函数://esbuild-register/src/node.tsimport{transformSync,TransformOptions}from'esbuild'import{addHook}from'pirates'exportfunctionregister(esbuildOptions:RegisterOptions={}){const{extensions=DEFAULT_EXTENSIONS,hookIgnoreNodeModules=true,hookMatcher,...overrides}=esbuildOptions//利用transformSyncconstcompile:COMPILE=functioncompile(code,filename,format){constdir=dirname(filename)constoptions=getOptions(dir)format=format??inferPackageFormat(dir,filename)const{code:js,warnings,map:jsSourceMap,}=transformSync(code,{sourcefile:filename,sourcemap:'both',loader:getLoader(filename),目标:options.target,jsxFactory:options.jsxFactory,jsxFragment:options.jsxFragment,format,...overrides,})//节省部分代码}constrevert=addHook(compile,{exts:extensions,ignoreNodeModules:hookIgnoreNodeModules,matcher:hookMatcher,})return{unregister(){revert()},}}观察上面的代码,我们可以看到在register函数内部,使用了esbuild模块提供的transformSyncAPI实现ts->js代码其实转换最关键的部分就是通过调用盗版库提供的addHook函数来注册编译ts文件的钩子。那么addHook函数内部是干什么的呢?让我们看一下它的实现://pirates-4.0.5/src/index.jsexportfunctionaddHook(hook,opts={}){letreversed=false;常量装载机=[];//存储新的加载器constoldLoaders=[];//存储旧的加载器letexts;constoriginalJSLoader=Module._extensions['.js'];//原始JSLoader//省略部分代码exts.forEach((ext)=>{//获取注册的loader,如果没有找到,默认使用JSLoaderconstoldLoader=Module._extensions[ext]||originalJSLoader;oldLoaders[ext]=Module._extensions[ext]];loaders[ext]=Module._extensions[ext]=functionnewLoader(mod,filename){让编译;if(!reverted){if(shouldCompile(filename,exts,matcher,ignoreNodeModules)){compile=mod._compile;mod._compile=function_compile(code){//这里需要恢复原来的_compile函数,否则会死循环mod._compile=compile;//执行编译前用户定义的钩子函数constnewCode=hook(code,filename);if(typeofnewCode!=='string'){抛出新错误(HOOK_RETURNED_NOTHING_ERROR_MESSAGE);}返回mod._compile(newCode,文件名);};}}oldLoader(mod,文件名);};});}其实addHook函数的实现并不复杂,该函数是通过替换mod._compile方法实现hook的功能,即在调用原来的mod._compile方法编译之前,hook(code,filename)函数会被调用,执行用户自定义的钩子函数,从而对代码进行预处理。对于esbuild-register库中的register函数,在执行hook函数时,会调用函数内部定义的compile函数编译ts代码,然后调用mod._compile方法编译生成的js代码.这里先介绍esbuild-register和pirates这两个库的内容。如果你想了解更多关于海盗库的工作原理,你可以阅读HowtoaddhookstotherequirefunctionofNode.js?本文。现在我们已经分析了esno.js文件,让我们分析esmo.mjs文件。esmo如何工作esmo命令对应esmo.mjs文件:#!/usr/bin/envnodeimportspawnfrom'cross-spawn'import{resolve}from'import-meta-resolve'constspawnSync=spawn.syncconstargv=process.argv.slice(2)resolve('esbuild-node-loader',import.meta.url).then((path)=>{process.exit(spawnSync('node',['--loader',path,...argv],{stdio:'inherit'}).status)})从上面的代码可以看出,在使用node应用程序执行ESModule文件时,--loader选项会被用于指定自定义ES模块加载器。--loader,--experimental-loader=...使用指定模块作为自定义加载器需要注意的是--loader选项指定的自定义加载器只适用于ESModule的导入调用,不适用于CommonJS的require调用。那么自定义加载器是做什么的呢?在当前最新的Node.jsv17.4.0版本中,还不支持以https://开头的说明符。我们可以在自定义加载器中使用Node.js提供的hook机制,让Node.js可以使用import来导入https://协议开头的ES模块。在分析如何自定义https资源加载器之前,我们需要引入importspecifiers的概念。importspecifierimport语句的说明符是from关键字之后的字符串,例如'path'inimport{sep}from'path'。说明符也用于exportfrom语句和作为import()表达式的参数。