现在我们用 React
和 Vue
开发项目时,package.json
文件都会有 babel
的依赖,还会依赖于一些插件。什么是 babel
,怎么样写一个 babel
插件呢,看完你就明白了。如果还有不懂或者文章有疑问的地方,欢迎留言~
背景知识 - 使用 babel 的原因和概念
使用 babel 原因 - 将 js 最新版本转成 es5 代码,兼容浏览器
es6/7/8
的出现,给我们带来了很多的方便,减少了很多代码的编写,但是不是所有的浏览器都是全部支持的,目前 Chrome 应该是支持率最高的。所以为了兼容市面上的浏览器,我们需要把我们的代码转成 es5,这应该是最初使用 babel 的缘由
使用 babel 原因 - 业务复杂化,需要很多定制化功能
随着业务的开发,我们会需要很多定制化的功能,单纯的 babel 并不能解决所有的问题,所以 babel 插件应用而来。
babel 是什么
Babel是一个工具链,主要用于在当前和较旧的浏览器或环境中将ECMAScript 2015+代码转换为JavaScript的向后兼容版本。以下是Babel可以为您做的主要事情:
-
Transform syntax 转换语法
-
Polyfill features that are missing in your target environment (through @babel/polyfill)
目标环境中缺少的Polyfill功能(通过@babel/polyfill)
-
Source code transformations (codemods)源代码转换
-
And more! (check out these videos for inspiration)
// Babel Input: ES2015 arrow function
[1, 2, 3].map((n) => n + 1);
// Babel Output: ES5 equivalent
[1, 2, 3].map(function(n) {
return n + 1;
});
更多概念参考 官方文档
背景知识 - AST (抽象语法树)
为什么要谈 AST?
如果你查看目前主流项目中的 devDependencies, 会发现不计其数的插件诞生,比如:Taro,mpvue等。我们归纳一下: javascript 转换、代码压缩、css预处理器、eslint、pretier等。有很多的 js 模块我们在生产环境不会用到,但是在开发过程中充当着重要的角色。所有的上述工具,都建立在 AST 这个巨人的肩膀上。
什么是 AST?
"It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code." 估计很多同学会和我一样,看完这段官方的定义一脸懵逼。OK,我们来看例子: Demo
var ddd = 12
var ddd = 12
这一句,首先我们知道这条语句是定义变量,定义了 ddd,并且赋值12,然后我们在对应看上图 AST 语法树,开始的 File 就是根节点, 然后是 program 节点,不用管,然后就是我们重点关注的 body节点。
VariableDeclaration
是变量声明,这就和我们前面的分析对应上了。接下来declarations
是一个数组,为什么是一个数组呢?其实也很好理解,我们变量声明的时候是不是可以用逗号,
隔开同时写多个变量呢,类似var ddd = 12, dddd1 = 13;
,而他们在同一条声明的语句中,所以就用数组来标识了。
在看具体的 VariableDeclarator
,里面一个 id
,一个是 init
,很直观就能看出 id 就是我们的变量名称,init 就是初始化的值,我们会看到有三个 key 在每个花括号中出现type start end
,type 就是类型,start 是代码的起始位置,end 是结束位置。
关于type 类型可以查看官网有详细介绍。
思考下面的简单函数 demo 可以怎么样进行解析呢?
Demo 具体解析可以自行思考
总结 AST
你会留意到 AST 的每一层都拥有相同的结构:
{
type: "FunctionDeclaration",
id: {...},
params: [...],
body: {...}
}
{
type: "Identifier",
name: ...
}
type: "BinaryExpression",
operator: ...,
left: {...},
right: {...}
}
注意:出于简化的目的移除了某些属性
这样的每一层结构也被叫做 节点(Node)。 一个 AST 可以由单一的节点或是成百上千个节点构成。 它们组合在一起可以描述用于静态分析的程序语法。
每一个节点都有如下所示的接口(Interface):
interface Node {
type: string;
}
字符串形式的 type 字段表示节点的类型(如: "FunctionDeclaration","Identifier",或 "BinaryExpression")。 每一种类型的节点定义了一些附加属性用来进一步描述该节点类型。
Babel 还为每个节点额外生成了一些属性,用于描述该节点在原始代码中的位置。
{
type: ...,
start: 0,
end: 38,
loc: {
start: {
line: 1,
column: 0
},
end: {
line: 3,
column: 1
}
},
...
}
每一个节点都会有 start,end,loc 这几个属性。
What’s babel plugin?
首先我们回顾下 babel 的处理流程: 解析(parse)、转换(transform)、生成(generate)
解析 - 解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:词法分析(Lexical Analysis) 和 语法分析(Syntactic Analysis)
词法分析
词法分析阶段把字符串形式的代码转换为 令牌(tokens) 流。.
你可以把令牌看作是一个扁平的语法片段数组:
n * n;
[
{ type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
...
]
每一个 type 有一组属性来描述该令牌:
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
...
}
和 AST 节点一样它们也有 start,end,loc 属性。.
语法分析
语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作。
转换 - 转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这就是 Babel 或是其他编译器中最复杂的过程,同时也是插件将要介入工作的部分
生成 - 代码生成步骤把最终的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)
具体如下图所示
How babel plugin work?
遍历(Traversal)
访问者会以深度优先的顺序, 或者说递归地对 AST 进行遍历,其调用顺序如下图所示:
在遍历过程中,由插件对自己感兴趣的节点类型, 进行转换操作,生成新的 AST
Babel 的转换步骤全都是AST的遍历过程
由于AST中的节点往往很多,如果每一个插件都对AST进行遍历 和转换,那可想而知效率极低,所以遍历一般使用访问者模式
访问者模式
提供一个作用于某种对象结构上的各元素的操作方式,使我们在不改变元素结构的前提下,定义作用于元素的新操作
访问者(Visitors)
访问者有以下作用:
- 进行统一的遍历操作
- 提供节点的操作方法
- 响应式维护节点之间的关系
进入节点时会调用 enter方法 离开节点时会调用 exit方法
Babel 插件怎么执行的?
按照插件定义的顺序来应用访问方法,比如在babel config注册了多个插件, babel-core 最后传递给访问器的数据结构如下图:
插件执行顺序:
plugin: 从前到后 presets:从后往前
【注意】:有时候需要注意一些插件的前后顺序
编写你的第一个简单 babel 插件
了解完前面的基础知识,我们就可以开始 babel 插件的开发。
插件格式
export default function({ types: t }) {
return {
visitor: {
Identifier(path, state) {},
ASTNodeTypeHere(path, state) {},
CallExpression(path, state) {}
}
};
};
上面是插件的基本格式,一个函数,参数是 babel,这里我们用到的是 types 这个属性,所以只把它写了出来,然后函数的返回是一个对象,key 是 visitor,对应值的对象是我们熟悉的东西,是 babel-types 的类型,然后是一个函数,函数的两个参数 path 表示路径,state 表示状态。
visitor 字面意思就是访问者,这里也是这个意思,表示我们要访问哪个类型的节点。这里的 CallExpression(调用表达式),类似于 hanle()。
简单插件实现
我们写一个简单的插件,把前面定义的变量名 ddd 换成是 a。
首先我们要找到定义变量的地方,然后判断变量名称是不是 ddd,如果是就把它替换成 a,大概思路就是这么简单。 继续打开我们的代码网站 Demo
代码左侧就是我们定义的变量 ddd,右侧就是 AST 语法树如下图。
我们看到定义变量的节点类型是 VariableDeclarator,所以写的代码应该是下面的模板
export default function({ types: t }) {
return {
visitor: {
VariableDeclarator(path, state) {}
}
};
};
我们只要判断 id 的 name 属性是不是 ddd 就行了,最后的代码如下:
export default function({ types: t }) {
return {
visitor: {
VariableDeclarator(path, state) {
if(path.node.id.name === 'ddd'){
path.node.id = t.Identifier('a')
}
}
}
};
};
有人可能会想这里是不是直接path.node.id.name = 'a'
就可以了,如果你是操作object,那你就对了,不过这里是 AST 语法树,所以想改变某个值,就是用对应的 AST 来替换
总结
我们写插件的时候只需要以下几个步骤就可以完成:
- 确认我们要修改的节点类型(把代码复制到 ASTExplorer 中,一一对应)
- 找到修改的属性是哪个(这里我们修改 id 属性)
- 根据旧的 AST 构建新的 AST 语句并替换
参考文献
接下来你可以去熟读Babel手册, 这是目前最好的教程, ASTExplorer 是最好的演练场,多写代码多思考。 你也可以去看 Babel的官方插件实现, 迈向更高的台阶。