什么是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()));
}
}
Babel是一个JavaScript转译器,将高级语法如ES6转换为ES5,确保兼容性。它涉及语法转换、polyfill注入、代码转换和静态分析。Babel的编译过程包括解析、转换和生成,基于AST进行操作。AST是源代码的抽象表示,Babel插件通过遍历和修改AST来转换代码。

277

被折叠的 条评论
为什么被折叠?



