前言
我们都知道在前端编译构建工具出现之前,前端项目采用的基本都是es5浏览器识别的语法来实现的。例如(jquery,es5 .......)。随着前端技术的发展(es6甚至更新的语法问世),然而浏览器是不能识别这些新语法的。那么就出现了编译构建工具,其中babel扮演着举足轻重的角色。
babel是什么?
官方介绍
Babel 是一个 JavaScript 编译器,Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
简单来说就是为了保证javascript在浏览器上正常运行,需要把浏览器不识别的语法转换成浏览器识别的语法,其中转换这一部分就是babel做的事情,在计算机编程中这一步骤也被叫做编译。
其实编译涉及的东西很多,有兴趣的同学可以了解一下《编译原理》,编译原理主要包括词法分析,语法分析,语义分析,中间代码生成,代码优化,目标代码生成这几大步骤,这里就不做过多介绍了,此处省略一百万字...
其实babel的工作流程和编译原理中的编译流程相对简单。我们可以归纳如下几个步骤:
词法分析
语法分析
代码转换
代码生成
babel的整体工作流程如下:
其中分为词法分析和语法分析两步可以合并成解析(parse)过程
抽象语法树(AST)
抽象语法树是高级编程语言(Java、JavaScript等)转换成机器语言的桥梁。解析器会根据ECMAScript 标准「JavaScript语言规范」来对代码字符串进行词法分析,拆分成一个个词法单元,再遍历各个词法单元进行语法分析构造出AST。我们通过如下代码来分析原理:
词法分析
词法分析阶段是对源代码进行"分词",他接收一段源代码,然后执行一段tokenize函数,把代码分割成tokens的东西。tokens是一个数组,由一些代码碎片组成,比如:数字、标点符号、运算符号等等,例如:
这里我们利用在线工具把上述代码进行词法分析的结果如下:
从词法分析结果可以看出,最终结果就是把代码解析成各个单词(let,age,+,=等等)
语法分析
在词法分析之后,语法分析会把词法分析得到的tokens转化为AST,有兴趣的可以阅读一下babel源码
AST抽象语法树是babel插件的核心概念,在编写自定义babel插件也会用到,因为在代码转换其实就是针对AST语法树各个节点进行的操作
下边推荐一个在线生成AST语法树工具
生成的AST太长,这里不展示了,有兴趣的可以在线尝试。
AST树,顾名思义数据结构中典型的一种数据类型-树,那么我们也知道,树都有一个根节点,也会有许多子节点。AST语法树是会有一个type值是Program的根节点,如下
经过观察子节点,其实子节点(包括根节点)都有相同的数据结构,如下
以上只是列举了几个不同类型的节点(注意:出于简化的目的移除了某些属性),其实AST语法树就是由这些节点组成的,它们组合在一起可以描述用于静态分析的程序语法。
从上边可以得出结论:每一个节点都有一个type字段代表节点的类型,还定义了一些附加属性用来进一步描述该节点类型。
babel编译
babel编译流程代码演示
上边我们也给出了babel编译代码的流程图,下边我们具体实践一下babel编译流程
这里先简单创建一个空项目,步骤如下:
创建一个文件夹,使用npm init -y创建package.json
然后在项目下创建src/index.js文件
然后在项目下创建src/index.js文件
为方便我们后边打断点debug,这里我们利用vscode工具给我们生成一个launch.js文件,添加自己的launch配置
我的launch.js内容如下:
具体配置请小伙伴们搜一下...
然后我们点想要断点的地方打上断点,击上图debug按钮运行即可,如下
更多关于vscode调试工具请自行学习,这里不做过多讲述
接下来正式回到babel编译正题,我们需要安装3个babel官方提供的插件
npm install-D@babel/parser@babel/generator@babel/traverse
@babel/parser 官方地址
@babel/traverse 官方地址
@babel/generator 官方地址
接下来了解一下这3个包的简单用法,修改src/index.js代码如下
以上只是简单的用代码形式演示了babel是如何编译代码的。
编译生成的代码如下
这里和源代码比较一下发现没有什么差别,因为我们没有使用插件对代码进行操作(压缩,混淆,优化等等)
@babel/parser包的parse方法传入源代码,进行词法分析合语法分析,最终生成AST抽象语法树
@babel/traverse包traverse方法接收AST抽象语法树并对其进行遍历(深度遍历),在此过程中对节点进行添加、更新及移除等操作。 这是Babel或是其他编译器中最复杂的过程,同时也是插件将要介入工作的地方,插件部分我们后边在讲
@babel/generator包generator方法接收的AST抽象语法树转换成字符串形式的代码,同时还会创建源码映射(sourceMap,根据传入的参数控制是否生成sourceMap)
上边也提到了,@babel/traverse的traverse转换过程是深度遍历整颗树对节点进行操作,它会访问树中的所有节点。这时候该方法第二个参数就起到作用了。这个参数是一个对象,对象每个属性是一个钩子函数。这个对象的属性值除了支持AST语法树节点的type值外,还有enter,exit;也就是在遍历每个节点的时候会先进入enter钩子函数,如果存在该节点对应的钩子函数,还会执行该钩子函数,最后在访问该节点结束的时候执行exit钩子函数...
修改转换代码如下:
再次debug运行代码
从上边打印结果可以看出,遍历到每个节点时都有执行enter,exit函数。合AST抽象语法树对比,也能看出确实属于深度优先递归遍历
接下来我们在添加VariableDeclaration钩子函数代码如下,
再次debug运行代码,VariableDeclaration函数会执行一次,因为我们这个AST语法树只有一个VariableDeclaration类型的节点。
到这里,相信很多小伙伴注意到了,钩子函数path参数是做什么的?
path代表着在遍历AST的过程中连接两个节点的路径,你可以通过path.node获取当前的节点path.parent.node获得父节点,它也提供了path.replaceWith,path.remove等API,这样就能通过一定条件来获取特点的节点进行修改了。
到这里可能有的小伙伴还有一个问题,babel可能定义了很多节点类型,我们怎么知道不同类型的节点是什么呢?
官方给出了所有类型点我查看类型,这里类型太多了,现用现查文档吧!!!
@babel/types
这里小编也推荐一个插件@babel/types,该插件包含非常多api,官方文档。它的作用是创建、修改、删除、查找ast节点。另外从上边知道AST的节点也是分为多种类型,比如ExpressionStatement是表达式、ClassDeclaration是类声明、VariableDeclaration是变量声明等等,同样的这些类型都对应了其创建方法:t.expressionStatement、t.classDeclaration、t.variableDeclaration,也对应了判断方法:t.isExpressionStatement、t.isClassDeclaration、t.isVariableDeclaration。这个插件往往和traverse遍历插件一起使用,因为types只能对单一节点进行操作,一般是在对节点的深度遍历中使用。
相信到这里,小伙伴们对babel编译原理已经有了基本了解,并且对AST抽象语法树也有了了解。下一边文章我们来实践一下怎么编写一个babel插件。