前言:AST已经深刻的存在咱们项目脚手架中,可是咱们缺不了解他,本文带领你们一块儿体验AST,感觉一下解决问题另外一种方法
在讲以前先简单介绍一下什么AST,抽象语法树(Abstract Syntax Tree
)简称 AST
,是源代码的抽象语法结构的树状表现形式。
平时不少库都有他的影子:
例如 babel
, es-lint
, node-sass
, webpack
等等。javascript
OK 让咱们看下代码转换成 AST
是什么样子。css
const ast = 'tree'
这是一行简单的声明代码,咱们看下他转换成AST的样子java
咱们发现整个树的根节点是 Program,他有一个子节点 body,body 是一个数组,数组中还有一个子节点 VariableDeclaration,VariableDeclaration中表示const ast = 'tree'
这行代码的声明,具体的解析以下:node
type: 描述语句的类型,此处是一个变量声明类型 kind: 描述声明类型,相似的值有'var' 'let' declarations: 声明内容的数组,其中每一项都是一个对象 ------------type: 描述语句的类型,此处是一个变量声明类型 ------------id: 被声明字段的描述 ----------------type: 描述语句的类型,这里是一个标识符 ----------------name: 变量的名字 ------------init: 变量初始化值的描述 ----------------type: 描述语句的类型,这里是一个标识符 ----------------name: 变量的值
大致上的结构是这样,body下的每一个节点还有一些字段没有给你们说明,例如:位置信息,以及一些没有值的key都作了隐藏,推荐你们能够去 asteplorer这个网站去试试看。webpack
总结一下, AST就是把代码经过编译器变成树形的表达形式。git
如何生成把纯文本的代码变成AST呢?编辑器生成语法树通常分为三个步骤es6
比方说上面的例子 const ast = 'tree'
,会被分析为const、ast、=、'tree'
github
const ast = 'tree'; [ { type: 'keyword', value: 'const' }, { type: 'identifier', value: 'a' }, { type: 'punctuator', value: '=' }, { type: 'numeric', value: '2' }, ]
当词法分析源代码的时候,它会一个一个字母地读取代码,因此很形象地称之为扫描-scans;当它遇到空格,操做符,或者特殊符号的时候,它会认为一个话已经完成了。web
2.语法分析:也称为解析器。它会将词法分析出来的数组转化成树形的表达形式。同时,验证语法,语法若是有错的话,抛出语法错误。数组
3.生成树:当生成树的时候,解析器会删除一些不必的标识tokens(好比不完整的括号),所以AST不是100%与源码匹配的,可是已经能让咱们知道如何处理了。说个题外话,解析器100%覆盖全部代码结构生成树叫作CST(具体语法树)
有不少的第三方库能够用来实战操做,能够去asteplorer这个网站去找你喜欢的第三方库,这里不限于javascript
,其余的语言也能够在这个网站上找到。
如图:
关于javascript
的第三方库,这里给你们推荐 babel
的核心库babylon
// yarn add babylon import * as babylon from 'babylon'; const code = ` const ast = 'tree' ` const ast = babylon.parse(code); // ast
ok,如今咱们已经知道如何把咱们的代码变成 AST
了,可是现实中,咱们常常会使用到代码的转换,比方说 jsx -> js, es6 -> es5, 是的就是 babel
,咱们来看看babel
是如何转换代码的。
大致上babel
转换代码分为三步
1. 经过`babylon`生成`AST` 2. 遍历`AST`同时经过指定的访问器访问须要修改的节点 3. 生成代码
看一个简单的例子一块儿理解一下
生成AST
import * as babylon from 'babylon'; // 这里也可使用 import parser from '@babel/parser'; 这个来生成语法树 const code = ` const ast = 'tree' console.log(ast); ` const ast = babylon.parse(code); // ast
遍历AST
同时经过访问器CallExpression
来访问console.log(ast)
并删除它
import traverse from '@babel/traverse' import t from '@babel/types'; // 2 遍历 const visitor = { CallExpression(path) { const { callee } = path.node; if ( t.isMemberExpression(callee) && callee.object.name === 'console' && callee.property.name === 'log' ) { path.remove(); } }, } traverse.default(ast, visitor);
生成新代码
import generator from '@babel/generator'; generator.default(ast);
简单的答疑:CallExpression表示这是一个调用,为何还要作更深刻的判断呢,由于直接的函数调用 foo() 这也是一个CallExpression,A.foo()这也是一个CallExpression, 因此要更深刻的判断
好的,代码转换完成!值得庆祝。咱们能够看到第一步生成AST
和第三步生成新代码都由babel
替咱们作了,咱们真正操做的地方在于第二步:经过访问器操做须要操做的节点。
因而可知咱们开发babel-plugin的时候,也只须要关注visitor这部分就好。
上述代码改成babel-plugin示例:
module.export = function plugin({ types: t}) { return { visitor: { CallExpression(path) { const { node } = path; if (t.isMemberExpression(node.callee) && node.callee.object.name === 'console' && node.callee.property.name === 'log' ) { path.remove(); } }, }, }; }
将这个插件加入到你的babel
插件列表中,能够看到它真的生效了,一切都是这么简单。so amazing!
开头提到的经常使用库prettire
, eslint
, css-loader
等等其实都是先生成AST
,而后再操做AST
,最后在生成代码。只不过操做AST
的过程很复杂,触类旁通在项目里,组件库升级,组件批量替换均可以使用这个思路。甚至能够根据业务作一些本身业务方的babel-plugin
都行。
感谢您的阅读,有问题能够在评论区交流~
帮助连接
如何开发一个babel-plugin
《AST for JavaScript developers》