什么是Babel?
Babel 是一个Javascript转译器,主要作用是将 ECMAScript 的高版本语法(例如 ES6)转换成低版本语法(例如 ES5),以便能够运行在当前和旧版本的浏览器或其他环境中。
Babel主要有以下几个方面的用途:
- 语法转换(转译esnext、typescript、jsx等)
- 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过引入第三方 polyfill 模块,例如 core-js)
- 代码转换(代码插桩,例如自动化埋点、变量替换等)
- 代码静态分析(各类linter工具、api文档生成、ts类型检查等)
Babel的编译流程简介
Babel编译是一个code to code的过程,整个流程分为三步:
- 解析(parse):将源码解析成抽象语法树(AST)
- 转换(transform):遍历AST,并使用babel api对AST节点进行增删改
- 生成(generate):将转换后的 AST 转换成代码,同时可以创建Source Map映射。
Babel AST
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。 — 来源百度百科
由于Babel的整个编译流程都是围绕着AST进行的,所以有必要了解一下AST的常见节点类型,需要强调的是,节点类型不需要死记硬背,需要的时候可以通过AST可视化平台查看。
AST可视化平台
这里对AST平台做简单的介绍,将Transform的开关打开可以看到平台分成了四个区域,对应了babel编译流程几个阶段的结果。
- 左上角区域:编写源代码
- 右上角区域:源代码经过parse得到的AST
- 左下角区域:编写babel插件,可以干预transform阶段,对AST节点增删改
- 右下角区域:展示转换后的 AST 转换成的代码
Identifier
Identifier表示代码中的标识符,如变量名、属性名、参数名,以及声明和引用
通过AST可视化平台可以方便的测试出代码中的哪些为Identifier类型。测试地址
Literals
Literals表示代码中的字面量类型,字面量包含数字、大整数、字符串、模版字符串、布尔值、null、正则表达式等
通过AST可视化平台测试代码中的哪些为Literals类型。测试地址
ps:JS中undefined也是一个字面量,但是在AST中并不是用Literals类型表示,因为它不像其他字面量那样有明确的值,而是一个标识符,它的值是未定义的,所以它的类型是Identifie。
Statements
Statements表示代码中的各类语句,如流程控制语句(while语句、for语句等)、条件语句(if语句、switch语句)、表达式语句、异常处理语句(try…catch…)。break、continue、return这类可以独立执行的代码都属于语句。
通过AST可视化平台测试代码中的哪些为Statements类型。测试地址
ps:声明语句不属于Statements,因为声明语句不会执行任何操作,而只是将变量、函数或类的定义放在程序中,以便被其他部分引用。
Declarations
Declarations表示代码中的声明语句,如变量声明、函数声明、import、export等
通过AST可视化平台测试代码中的哪些为Declarations类型。测试地址
Expressions
Expressions表示代码中的表达式,指那些执行完有结果的语句(区别于Statements),如表达式,函数调用,数组,对象等
通过AST可视化平台测试代码中的哪些为Expressions类型。测试地址
想了解全部的Babel AST可以在babel仓库的AST文档里查看
Babel提供的APi
Babel在编译流程的三步中,每一步都暴露了一些api,所有api可在babel api文档里查看。
- parse阶段提供了
@babel/parse
,包含源码转换成AST的方法 - transform阶段提供了
@babel/traverse
,包含遍历AST和对AST节点增删改的方法 - generate阶段提供了
@babel/generator
,包含将AST转换成代码并生成source map的方法
实现babel插件主要关注transform阶段,所以接下来主要针对这个阶段介绍相关的api
AST的遍历过程
babel traverse提供了traverse方法function traverse(ast, options)
,ast指定要遍历的节点,options里制定visitor函数。visitor函数以AST的类型命名,babel在遍历AST的过程中会在每个节点调用对应类型的visitor函数。一个visitor函数也可以同时用于多种AST节点的处理,只要将AST节点类型用|连接。
babel在遍历AST的时候会经过每个AST节点两次,分别为进入(enter)和离开(exit)两个阶段,可以对一个节点类型的两个阶段分别指定回调函数,如果只指定了一个函数那就是在enter阶段调用。
visitor函数path参数
AST是颗树状结构,path用于记录一个节点路径。它可以用来构建从根节点到当前节点的路径。
如图,将一颗4层的AST局部放大,可以看到4个AST节点通过三个path关联起来,node记录了AST节点信息。
其中,
- path1关联了node0和node1,并可以通过get方法关联到path2和path3
- path2关联了node1和node2,并可以通过parentPath关联到path1、通过getNextSibling方法关联到path3
- path3关联了node1和node3,并可以通过parentPath关联到path1、通过getPrevSibling方法关联到path2
通过这样的关联关系就可以方便的串起整颗AST,遍历AST。
从上图可以看出,path 对象上提供了很多属性和方法,常用的可以分为以下几种类型:
获取节点信息、父子、兄弟节点path相关
path.node
指向 AST 节点path.parent
指向父级 AST 节点path.getPrevSibling
获取上一个兄弟节点对应的pathpath.getNextSibling
获取下一个兄弟节点对应的pathpath.find
从当前节点向祖先节点查找节点对应的pathpath.get
获取属性的path,可获取到子节点对应的pathpath.set
设置属性的pathpath.scope
获取当前节点的作用域信息
增删改AST相关
path.insertBefore
在节点之前插入节点path.insertAfter
在节点之后插入节点path.replaceWith
将节点替换另一个节点path.replaceWithMultiple
将当前节点替换多个节点replaceWithSourceString
以源代码的形式替换节点path.remove
删除节点
判断AST类型相关
path.isXXX
判断节点是不是XXX类型path.assertXXX
判断节点是不是XXX类型,不是则抛出异常
遍历节点相关
path.traverse
从当前节点开始遍历path.skip
跳过当前节点的子节点的遍历path.stop
结束后续节点遍历
visitor函数state参数
state参数用于不同节点间传递数据,同时包含了插件的options和file信息
state.set
记录数据state.get
获取数据state.opts
插件入参的options参数state.cwd
当前插件运行所处当前工作目录state.file
指向当前运行的文件
其他AST相关API
Babel在transform阶段为了化简AST的创建逻辑还提供了@babel/types
、@babel/template
@babel/types
提供判断AST类型和创建AST节点的方法@babel/template
提供批量创建AST的方法
Babel插件实战:实现一个简单的DefinePlugin
DefinePlugin是一个常见的Webpack插件,他允许在编译时将你代码中的变量替换为其他值或表达式。我们可以通过babel插件实现相同的能力。
根据webpack官网的描述,传递给 DefinePlugin 的每个键都是一个标识符或多个以.连接的标识符。
- 如果该值为字符串,它将被作为代码片段来使用。
- 如果该值不是字符串,则将被转成字符串。
- 如果值是一个对象,则它所有的键将使用相同方法定义。
- 如果键添加 typeof 作为前缀,它会被定义为 typeof 调用。
DefinePlugin对值做了比较多的处理,而这篇文章目的是学习babel插件写法,所以仅对值做简单的toString处理。
const DEFINE = {
PRODUCTION: JSON.stringify(true),
VERSION: JSON.stringify('5fa3b9'),
BROWSER_SUPPORTS_HTML5: true,
TWO: '1+1',
'typeof window': JSON.stringify('object'),
'process.env.NODE_ENV': JSON.stringify("pre"),
}
区分目标AST类型
根据DefinePlugin的描述,键主要分为三种类型,我们需要找出每种类型对应的AST节点类型,不需要查文档,在AST可视化平台简单测试一下即可得到结果:
【普通标识符】为Identifier类型
【多个以.
连接的标识符】为MemberExpression类型
【以typeof操作符为前缀】为UnaryExpression类型
匹配目标AST
匹配普通标识符
对于普通标识符,观察其AST可以发现Identifier类型节点上有个name字段记录了标识符字符串,所以可以通过这个值于DefinePlugin入参的键做匹配,vistor函数如下
{
Identifier(path) {
if (DEFINE[path.node.name]) {
// todo
}
}
}
匹配多个以.
连接的标识符
可以看到多个以.
连接的标识符的AST相对比较复杂,一种思路是通过对MemberExpression类型往下递归匹配,这种方式实现和理解成本相对较高。我们还有一种更简单的匹配方法,可以将MemberExpression类型AST generate成字符串code,这样就可以通过字符串与键做匹配,vistor函数如下
{
MemberExpression(path) {
if (DEFINE[path.toString()) {
// todo
}
}
}
ps:path的toString方法就相当于将AST generate成字符串
匹配以typeof操作符为前缀的标识符
可以看到UnaryExpression类型 AST节点上有个operator属性可以判断操作符类型,当operator属性值为’typeof’,就可以将AST 转成字符串code与键匹配,vistor函数如下
{
UnaryExpression(path) {
if (path.node.operator === 'typeof' && DEFINE[path.toString()) {
// todo
}
}
}
修改目标AST
修改AST我们也需要实现符合AST节点结构的对象,手动实现是比较麻烦的,可以借助上面提到的@babel/types
来创建AST节点,代码如下
const t = require('@babel/types');
const replace = (path, key) => {
const value = DEFINE[key];
if (value) {
path.replaceWith(t.Identifier(value.toString()));
}
}