Babel是前端很常用的转码器,更准确地说是转译器,是从源码到源码的转换编译器,例如可以将我们按照ES6标准写的代码转为ES5标准,也就是说可以直接使用ES6的最新标准来编写脚本,而不用担心现有环境是否支持此标准。
例如:Babel可以将我们最常用的箭头函数:
const demo = item => item + 1;
转译成ES5的函数写法:
const demo = function(item){
return item + 1;
}
将ES6标准转译成ES5,不用担心各大浏览器是否已经支持ES6的最新标准这个确实是解决了我们工作中一大问题,但是Babel的功能并不止于此,它是可以转译很多种语法的,例如我们最常用的react中JSX的语法,而且Babel的核心就是利用插件,通过不同的插件可以转译不同的语法,让我们可以畅快的去尝试最新的语法。
Babel的三大主要步骤
我们先来看一张图,这是我网上找来的,我感觉这张图已经把Babel的工作步骤画的很清楚了。
从上图可以看出Babel的三大步骤分别是:解析(parse),转换(transform),生成(generate)。嘿嘿,我发现图中的英文有点问题,大家可以查一下是不是,如果是我翻译错了请指正。
什么是AST?
在详细解释这三大步骤前,我们有必要先来了解一下什么是AST?
“ AST ”其实叫做“ 抽象语法树 ”,是源代码的抽象语法结构的树状表现形式,其实个人觉得babel对于AST和我们熟悉的jquery对于DOM有点像。我们可以想象一下如何将JS代码用树状表示出来。
var a = 1 + 1
var b = 2 + 2
上面声明了两个变量,如何用树状表示他们呢?首先一定会有东西可以代表这些声明、变量名、常量等等的信息。很明显,这棵树上有两个变量,两个变量名a和b,有两个运算语句,操作符都是+号。但是有了这些还不够,既然是树,树枝连树枝,还必须建立起彼此之间的关系,比如一个声明语句,声明类型是var,左侧是变量名,右侧是表达式,有了这些信息我们就可以还原这个程序了。这个就是把源码解析成AST时所做的事情了。
在AST中我们用node(节点)来表示每个代码片段,比如上面程序的整体就是一个节点(Program,所有的AST根节点都是Program节点),然后下面有两条语句,所以它的body属性上就两个声明节点VariableDeclaration。所以上面程序的AST类似这样:
从图上可以看出节点上用了各个属性来表示各种信息以及程序之间的关系。
解析(parse):
在大概了解了AST是个啥东西后,我们可以来了解三大步骤了,首先是第一步解析。主要是为了接收代码并输出AST,也就是将代码变为树状,这个步骤又分两个阶段:词法分析(Lexical Analysis)和 语法分析(Syntactic Analysis)。
词法分析
词法分析阶段是把字符串形式的代码转换成令牌(tokens)流。你可以把tokens看成是一个语法片段数组。例如:n*n代码经过词法分析阶段后转换成了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
},
...
}
语法分析
语法分析阶段是把一个令牌(tokens)流转换成AST的形式,以便于后续的操作。
转换(transform)
第二步是转换,主要是用来接收解析好的AST并对其进行遍历,在此过程中对节点进行添加、更新或者移除等操作,准确的说是利用我们配置好的plugins/presets把Parser生成的AST转变为新的AST,这一步是插件要介入工作的部分,从三大步骤的图中也可以看出占了很大一块的比重,足以看出这个转换过程就是Babel中最复杂的部分,我们平时配置的plugins/presets就是作用在这里了。
生成(generate)
第三步是生成,最后一步把最后经过一系列转换后的最新AST转换成字符串形式的代码,同时还会创建源码映射(source maps)。代码生成反而比较简单,只要深度优先遍历整个AST,然后构建可以表示转换后代码的字符串就可以了。
Visitors(访问者)
当我们说到“进入”一个节点时,其实是在说我们在访问它,之所以使用这样的术语是因为有一个访问者模式的概念。
这里的访问者是一个用于AST遍历的跨语言的模式。简单来说就是一个对象,定义了用于在一个树状结构中获取具体节点的方法,例如:
const MyVisitor = {
Identifier: {
// 当进入Identifier节点的时候执行
enter() {
console.log("Entered");
},
// 当退出Identifier节点的时候执行
exit() {
console.log("Exited!");
}
}
};
每一个节点都会有自己对应的type,比如变量节点Identifier等。上例中我们给babel提供了一个MyVisitor对象,在这个对象上面我们以这些节点的type做为key,已一个函数作为值,这样在遍历进入到对应节点时,babel就会执行对应的enter函数,向上遍历退出对应节点时,babel就会去执行对应的exit函数。
Paths(路径)
我们通过visitor可以在遍历到对应节点执行对应的函数,可是要修改对应节点的信息,还是不够,毕竟要增删节点,我们不能等进入节点了才执行,我们还需要拿到对应节点的信息以及节点和所在的位置(即和其他节点间的关系), visitor在遍历到对应节点执行对应函数时候会给我们传入path参数,辅助我们完成上面这些操作。Path 是表示两个节点之间连接的对象,而不是当前节点,我们上面访问到了Identifier节点,它传入的 path参数看起来是这样的:
{
"parent": {
"type": "VariableDeclarator",
"id": {
...
},
....
},
"node": {
"type": "Identifier",
"name": "..."
}
从上例可以看出:path.node.name可以获得当前节点的name,path.parent.id可以获得父节点的id,另外path对象上面还包含了添加、更新、移动和删除节点有关的很多方法,至于这些有关的方法就不再这里展开了,可以看文档解决。上面说visitor在遍历到对应节点执行对应函数时候会给我们传入path参数,所以我们可以根据这个修改一下上文中的MyVisitor函数:
const MyVisitor = {
Identifier: {
// 当进入Identifier节点的时候执行
enter(path) {
console.log('traverse enter a Identifier node the name is ' + path.node.name);
},
// 当退出Identifier节点的时候执行
exit(path) {
console.log('traverse exit a Identifier node the name is ' + path.node.name);
}
}
};
这样我们就可以操作想要改变的节点了,嗯嗯~~ very good!!
总结
最后我们总结一下,Babel最重要的就是熟悉它的工作步骤,也就是它的原理:
- 接收源代码
- 将源代码转成字符串形式
- 把字符串形式的源代码转换成令牌流
- 把一个令牌流转换成AST的树状形式。
- 接收AST并对其进行遍历,在此过程中可以对节点进行各 种操作,比如添加、更新、移除等等。
- 深度遍历最终的AST树,然后构建可以表示转换后代码的字符串,并且同时创建源代码映射。
其实很多猿兄都和我一样,刚接触babel的时候,直接上手用,看着文档知道如何用,但是不知背后的原理,今天这一片笔记也是看了好几篇文档和大牛的博客整理出来的比较关键的几点,看上去简简单单的转译器,其实背后的实现还是挺不容易的,我们已经简单的分析了代码,并且可以修改一些抽象语法树上的内容来达到我们的目的,不过开头的时候也说了对于Babel而言插件是很重要的,现阶段Babel已经不仅仅是去转换ES6了,最常用的还有转换react中JSX的语法,所以除了懂得原理以外,我们也可以自己实际去编写一些有意思的插件来应用与自己的工作中,更好的提高对Babel的理解,今天介绍就到这里,还是那句话如有总结不到位的,希望各位猿兄指教。