当前位置: 首页 > Web前端 > JavaScript

基于AST的代码自动生成解决方案

时间:2023-03-27 11:34:36 JavaScript

最近接到一个请求,需要通过第三方提供的d.ts文件来定义对应的JSSDK文件。形式如下:第三方提供的d.ts文件:exportclassSDK{start(account:string);关闭();init(id:string):Promise<{结果:数字;}>}定义JSSDK文件://初始化wrapper对象,省略细节constwrapper=(wrap)=>wrap;//定义JSSDKconstSDK={asyncstart({account}){returnawaitwrapper.start(account)},asyncclose(){returnawaitwrapper.close(account)},asyncinit({id}){returnawaitwrapper.init(id)},}exportdefaultSDK;项目之初,我们根据第三方提供的d.ts文件手动编写了JSSDK。由于这个d.ts经常变动,所以需要不断同步JSSDK;同时,由于我们的项目是多人维护的,手写的JSSDK难免会有很多冲突,不利于研发效率。的。通过分析d.ts及其对应的JSSDK,我们可以看出它们的格式基本固定,并且有着非常明确的对应关系。那么我们可以思考一下,是否可以通过自动化的方式直接从d.ts中生成对应的JSSDK?比较简单的思路是逐行分析d.ts代码,通过正则化等方式匹配关键词,获取关键信息。这种方法简单粗暴,但不够优雅。它需要非常复杂的匹配规则才能满足要求。一旦d.ts格式发生变化,原有的匹配规则可能无法直接使用,维护成本过高。为了避免格式变化带来的一系列问题,“抽象”可以说是一个相对更合适的解决方案。代码的AST是一种抽象方法,可以有效避免格式和写法变化带来的影响,将源代码转换成树状结构的数据,方便脚本读取,方便后续操作。d.ts的AST分析由于d.ts也是一个typescript文件,我们可以使用typescript官方提供的API来生成对应的AST://https://ts-ast-viewer.com/constdTsFile=fs.readFileSync(resolve(__dirname,filePath),'utf-8')constsourceFile=ts.createSourceFile('sdk.ts',//自定义一个文件名dTsFile,//源代码ts.ScriptTarget.Latest//编译版本)我们也可以通过网站https://ts-ast-viewer.com来查看生成的源文件(AST)是否符合预期:有了AST,我们需要分析里面需要哪些信息。从前面d.ts转JSSDK的例子可以看出,d.ts中最重要的是要知道两件事:定义了哪些方法;方法中传递了哪些参数。从AST可以知道ClassDeclaration下的MethodDeclaration是d.ts定义的一系列方法;而MethodDeclaration中的Parameter定义了方法的参数。接下来是不是要读取AST的节点信息,然后直接生成JSSDK呢?答案是否定的。原因是如果把“解析AST”和“生成JSSDK”的逻辑耦合在一起,由于AST节点数量多,类型丰富,可能需要进行大量的条件判断,最终的逻辑会是很混乱。是一种“看一点,做一点”的感觉,不过和逐行读取d.ts然后生成JSSDK的思路没什么区别。为了避免这种过度耦合带来的难以维护的问题,我们可以引入一种“领域特定语言(DSL)”。在JSSDK中使用DSL生成DSL的定义,可以参考这篇文章《开发者需要了解的领域特定语言(DSL)》。DSL的定义听上去很厉害,但其实说白了,就是定义一种可以连接过去和未来的过渡格式。在我们的场景中,我们可以定义一个JSON格式的DSL来记录从AST中提取的关键信息,然后从这个DSL中生成所需的JSSDK文件。这种方式看似多了一步工作,增加了工作量,但是在实际使用中,你会发现它对于逻辑的解耦很有帮助,对于后续的维护也是大有裨益的。对于我们的示例:exportclassSDK{start(account:string);关闭();init(id:string):Promise<{结果:数字;}>}通过分析它的AST,可以组织成这样的DSL:constDSL=[{name:'start',parameters:[{name:'account',type:'string'}]},{name:'close',parameters:[]},{name:'init',parameters:[{name:'id',type:'string'}]}]DSL清楚地记录了方法的名称和参数,如果需要的话,您可以轻松添加更多信息,例如返回值类型等。接下来分析JSSDK的格式:constwrapper=(wrap)=>wrap;//定义JSSDKconstSDK={asyncstart({account}){returnawaitwrapper.start(account)},asyncclose(){returnawaitwrapper.close(account)},asyncinit({id}){returnawaitwrapper.init(id)},}exportdefaultSDK;由于格式也是固定的,所以只需要准备一个字符串模板,然后遍历DSL,将组织好的方法填入模板:constapiArrStr=DSL.map(api=>{//伪代码,省略了这一步信息提取return`async${name}(${params}){returnawaitwrapper.${name}(${params})}`})consttemplate=`constSDK={${apiArrStr}}exportdefaultSDK;`返回模板;d.ts代码,然后自动生成相应的JSSDK方法,并引入DSL的概念,进一步解决逻辑耦合问题,希望能给读者一些启发。