前端 AST 详解,手写 babel 插件
🥙一、前言
抽象语法树(Abstract Syntax Tree,AST),是源代码(不仅限于 JavaScript,同时还应用于其他语言,例如: Python,Rust 等)语法结构的⼀种抽象表示。它以树状的形式表现编程语⾔的语法结构,树上的每个节点都表示源代码中的⼀种结构。AST 运⽤⼴泛,⽐如:
⾼级语⾔的编译、机器码的⽣成⼀些⾼级编辑器的错误提示、代码⾼亮、代码⾃动补全;
对于前端来说很多⼯具,例如 elint 、 pretiier 对代码错误或⻛格的检查,babel、typescript 对代码的编译处理等等。
🥪二、节点介绍
本文示范数据:
type:标识节点的类型。
Identifier(标识符):简单来说就是我们写 JS 时自定义的名称,如变量名,函数名,属性名,都归为标识符,值存放于字段 name 中。
CallExpression(函数表达示):比如:setTimeout(()=>{})。callee 属性是一个表达式节点,表示函数,arguments 是一个数组,元素是表达式节点,表示函数参数列表.
MemberExpression(成员表达式节点):即表示引用对象成员的语句,object 是引用对象的表达式节点,property 是表示属性名称,computed 如果为 false,是表示
.
来引用成员,property 应该为一个 Identifier 节点,如果 computed 属性为 true,则是 [] 来进行引用,即 property 是一个 Expression 节点,名称是表达式的结果值。window.a 对应的 AST 如下:AssignmentExpression(赋值表达式节点):operator 属性表示一个赋值运算符,left 和 right 是赋值运算符左右的表达式
ArrayExpression(数组表达式节点): interest:["篮球","羽毛球"],
elements
属性是一个数组,表示数组的多个元素,每一个元素都是一个表达式节点。VariableDeclaration(变量声明表达式):kind 属性表示是什么类型的声明,值可能是 var/const/let。declarations 表示声明的多个描述,因为我们可以这样:
let a = 2,b=3
。VariableDeclarator(变量声明的描述):id 表示变量名称节点,init 表示初始值的表达式,可以为 null
IfStatement(if 表达式):if(true),test 属性表示 if (...) 括号中的表达式。
consequent 属性是表示条件为 true 时的执行语句,通常会是一个块语句。
alternate 属性则是用来表示 else 后跟随的语句节点,通常也会是块语句,但也可以又是一个 if 语句节点,即类似这样的结构:if (a) { //... } else if (b) { // ... }。alternate 当然也可以为 null。
Literals 字面量
StringLiteral 字符串字面量("foo")
NumericLiteral 数值字面量(123)
BooleanLiteral 布尔字面量 (true)
TemplateLiteral 模板字面量 (${obj})
🌮三、Babel 基础
Babel 是一个 JavaScript 的转译器,其执行过程就是一个编译转换的过程。作为一个 js 转译器,babel 暴露了很多 api,利用这些 api 可以完成源代码到 AST 的 parse,AST 的遍历与处理以及目标代码的生成。babel 将这些功能的实现放到了不同的包里面,下面逐一介绍。
@babel/parser
解析源码得到 AST@babel/traverse
遍历 AST 节点@babel/types
用于构建 AST 节点和判断 AST 节点类型@babel/generate
打印 AST,生成目标代码和sorucemap
(即将 ast 转换成 js 代码)
babel 的处理步骤:主要有三个阶段:解析(parse), 转换 (transform),生成(generate)。
parse 将源码转成 AST,用到
@babel/parser
模块。transform 对 AST 进行遍历,在此过程中对节点进行添加、更新及移除等操作。因此这是 bebel 处理代码的核心步骤,是我们的讨论重点,主要使用
@babel/traverse
和@babel/types
模块。generate 打印 AST 成目标代码并生成
sourcemap
,用到@babel/generate
模块。
接下来我们来重点了解转换这一步,上面我们提到,转换的第一步是遍历 AST。说到这里就不得不提到一个设计模式——访问者模式。
访问者模式,即将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式,简单来说,就是定义了用于在一个树状结构中获取具体节点的方法。当访问者把它用于遍历中时,每当在树中遇见一个对应类型时,都会调用该类型对应的方法。
🍰四、案例展示
从 babel7 开始,所有的官方插件和主要模块,都放在了 @babel 的命名空间下。从而可以避免在 npm 仓库中 babel 相关名称被抢注的问题,并且采用了 Babel Monorepo 风格的仓库。在测试之前需要安装@babel/core
、@babel/cli
、@babel/preset-env
@babel/core
是 Babel 实现转换的核心,他是依赖能力更底层的 @babel/parser
、 @babel/code-frame
、@babel/generator
、@babel/traverse
、@babel/types
等。
@babel/parser: 接受源码,进行词法分析、语法分析,生成 AST。
@babel/traverse:接受一个 AST,并对其遍历,根据 preset、plugin 进行逻辑处理,进行替换、删除、添加节点。
@babel/generator:接受最终生成的 AST,并将其转换为代码字符串,同时此过程也可以创建 source map。
@babel/types:用于检验、构建和改变 AST 树的节点
@babel/cli
是 Babel 提供的命令行,它可以在终端中通过命令行方式运行,编译文件。@babel/preset-env'
Babel 只是一个'编译器'你需要告诉他转换规则,需要在 transformer,利用我们配置好的 plugins/presets 把 Parser 生成的 AST 转变为新的 AST,即@babel/preset-env'
就是一套转换规则集合。下图为转换流程let
声明转换为var
声明
通过 parse 解析得到了ast
,具体如下:
执行
输出
可见var
都变成了let
🍔五、手写 babel 插件
该插件为superLog
,源码如下:
//callee
//path.node.arguments 的值
新建 trans,js 文件
其中 node.type 分别是:
最后打印结果为
版权声明: 本文为 InfoQ 作者【不叫猫先生】的原创文章。
原文链接:【http://xie.infoq.cn/article/3774bd7b937437e274b24291a】。文章转载请联系作者。
评论