豆腐粉,我们又见面了。今天一期,字节跳动数据平台的芋头酱就带你走进AST的世界。作者:芋头酱什么是AST抽象语法树(AbstractSyntaxTree,AST)是源代码抽象语法结构的树状表示,对应于具体语法树;之所以是抽象的,是因为抽象语法树并没有表达出真实语法中出现的每一个细节,它是语法无关的,语言无关的;AST可以想象成一套标准化的编程语言接口定义,但这套规范是针对编程语言的。从小的变量声明到复杂的模块,一切都可以用这套规范来描述。感兴趣的同学可以深入了解AST的概念和原理。本文重点介绍JavaScriptAST的应用。为什么要讲AST对于前端同学来说,在日常开发中,AST相关的场景无处不在;例如:webpack、babel、各种lints、prettiers、codemods等,都是基于AST处理的;掌握AST相当掌握代码控制代码的能力可以帮助我们开阔思路和视野。无论是写框架,还是写工具和逻辑,AST都会成为你的得力助手。AST分析过程首先推荐一个AST在线转换网站:astexplorer.net,收藏一下很重要;除了js,还有很多其他语言的AST库;无需任何配置即可用作游乐场;在讲解案例之前,先了解一下解析过程,分为三个步骤:源码-->ast(源码解析为ast)traverseast(遍历ast,访问树中的每个节点,对节点进行各种操作)ast-->code(将ast转换成源码,收工)将源码解析成AST的引擎有很多,转换后的AST类似;UseCases以一个变量声明开始,如下:constdpf='DouPiFan';将代码复制到astexplorer中,得到如下结果(结果已简化),这张图说明了从源代码到AST的过程;选择不同的第三方库生成AST,结果会有所不同,这里以babel/parse为例;前端同学对babel比较熟悉,但是通过它的处理,可以在浏览器中支持ES2015+代码。这只是babel的应用场景之一。对自己的官方定位是:Babel是一个javascript编译器。回到babel-parser,它使用Babylon作为解析引擎,也就是AST到AST的操作。babel在babylon的基础上封装了解析(babel-parser)和生成(babel-generator)这两个步骤,因为每个操作都会做这两个步骤;对于应用程序,操作的重点是AST节点的遍历和更新;第一个babel插件,我们以最简单的babel插件为例,了解它的处理过程;当我们开发使用babel-plugin的时候,只需要在visitor中描述如何转换AST即可。将它添加到你的babel插件列表中,它就会工作。我们的第一个babel插件开发完成;babel-plugin-import是如何实现的?用过antd的同学都知道babel-plugin-import插件,用于按需加载antd组件。配置后的效果如下:import{Button}from'antd'↓↓↓↓↓↓importButtonfrom'antd/lib/button'本文旨在抛砖引玉。插件的实现细节和各种边界条件可以参考插件的源码;用AST思维思考,实现步骤如下:找到代码中的import语句,一定是import{xxx}from'antd'将第1步找到的节点转换为importButtonfrom'antd/lib/button'实现步骤打开神器:ASTExplorer,将第一行代码复制到神器中,点击代码中的import关键字,会自动定位到对应的节点,结构如下:ImportDeclaration{type:"ImportDeclaration",specifiers:[{//对应{}括号内的组件"antd"},...}源码转换成对象,有类型和属性,无论是关键字,变量声明,还是字面值,都有对应的类型;import语句对应的类型为:ImportDeclaration{Button}对应说明符数组,例子中只引入了“Button”,所以specifiers数组中的元素只有一个specifiers元素,即Button,其类型为ImportSpecifier;'antd'在源节点中,类型为StringLiteral,值为antd。再次注意:例子并不是完整的逻辑实现,细节和边界条件,可以参考源码或者自己完善;对AST的操作类似于浏览器自带的DOMAPI;首先确定要搜索的节点类型,然后根据具体情况缩小搜索范围,最后对找到的节点进行增删改查;//babelplugintemplateexportdefaultfunction({types:t}){return{//Visitor中的每个函数接收2个参数:路径和状态visitor:{ImportDeclaration(path,state){const{node}=path;//源值为antdif(node.source.value==='antd'){constspecifiers=node.specifiers//遍历specifiers数组constresult=specifiers.map((specifier)=>{constlocal=specifier.local//构造源constsource=t.stringLiteral(`${node.source.value}/lib/${local.name}`)//构造导入语句returnt.importDeclaration([t.importDefaultSpecifier(local)],source)})console.log(result)path.replaceWithMultiple(result)}}}}}验证方法也很简单,将这段代码复制到ASTExplorer中查看输出结果即可;到这里,这个“简单”的插件的实现就完成了;再回顾一下实现思路:比较源码在语法树上的差异,明确哪些转换和修改分析类型,可以在babel官方的插件模板中找到类型说明,通过visitor访问对应类型节点,增删改查Codemod上面讲解了babel中ast的基本操作方法,下面就来看看在codemod中使用antd3的同学吧。antd3的代码转换成antd4的工具库;因为它的本质就是进行代码转换,所以基于babel实现codemod是完全可以的。但除了代码转换,你还需要命令行操作、源码阅读、批量执行转换、日志输出等功能。它是一个函数的集合,代码转换是其中很重要的一部分;因此,推荐另一个工具jscodeshift。它的定位是一个transformrunner,所以我们的核心工作就是定义一系列的transform,也就是transformrules,剩下的命令行、源码读取、批量执行、日志输出都可以交给jscodeshift。准备工作首先定义一个transform,这个和babel插件import{Transform}from"jscodeshift"很相似;consttransform:Transform=(file,api,options)=>{returnnull;};导出默认转换;动手实践我们尝试把Button组件的“type”属性替换为“status”,并在style中添加width属性://inputconstComponent=()=>{return(
