前言
我们都知道在前端编译构建工具出现之前,前端项目基本都是用es5
浏览器识别的语法来实现的。(jquery
,es5
...)。随着前端技术的发展(es6
甚至更新语法的问世),浏览器是不能识别这些新语法的。那么就出现了编译构建工具,其中babel
扮演着举足轻重的角色。那么下边我们来探索一下babel
究竟是什么?
小编推荐福利,精彩内容请点击链接,点击这里
babel是什么?
官方介绍
Babel 是一个 JavaScript 编译器。
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
简单的说就是为了保证javascript
在浏览器上正常运行,需要把浏览器不识别的语法转换成浏览器识别的预发,其中转换这一步骤就是babel
做的事情,在计算机编程中这一步骤也会被叫做编译。
其实编译涉及的东西很多,有兴趣的同学可以了解一下《编译原理》,编译原理主要包括词法分析,语法分析,语义分析,中间代码生成,代码优化,目标代码生成这几大步骤,这里就不做过多介绍了,此处省略一百万字...
其实babel
的工作流程和编译原理中的编译流程相对简单。我们可以归纳如下几个步骤:
- 词法分析
- 语法分析
- 代码转换
- 代码生成
babel的整体工作流程如下图:
其中分为词法分析和预发分析两步可以合并成解析(parse)过程
从上图可以看到编译从开始到结束有一个最重要的东西,抽象语法树/AST的知识,以下简称AST
,babel
编译代码的整个流程都离不开它。
抽象语法树(AST)
抽象语法树是高级编程语言(Java
、JavaScript
等)转换成机器语言的桥梁。解析器会根据ECMAScript 标准「JavaScript语言规范」来对代码字符串进行词法分析,拆分成一个个词法单元,再遍历各个词法单元进行语法分析构造出AST。我们通过如下代码来分析原理:
let age = 10;
age = age + 20;
词法分析
词法分析阶段是对源代码进行“分词”,它接收一段源代码,然后执行一段tokenize
函数,把代码分割成被称为tokens
的东西。tokens
是一个数组,由一些代码的碎片组成,比如数字、标点符号、运算符号等等等等,例如这样:
这里我们利用在线工具把上述代码进行词法分析的结果如下:
词法分析工具
[
{ "type": "Keyword", "value": "let"},
{ "type": "Identifier", "value": "age"},
{ "type": "Punctuator", "value": "="},
{ "type": "Numeric", "value": "10"},
{ "type": "Punctuator", "value": ";"},
{ "type": "Identifier", "value": "age"},
{ "type": "Punctuator", "value": "="},
{ "type": "Identifier", "value": "age"},
{ "type": "Punctuator", "value": "+"},
{ "type": "Numeric", "value": "20"},
{ "type": "Punctuator", "value": ";"}
]
从词法分析结果可以看出,最终结果就是把代码解析成各个单词(let,age,+,=等等)
babel-tokenizer方法实现
语法分析
在词法分析之后,语法分析会把词法分析得到的tokens
转化为AST
,有兴趣的可以阅读一下babel
源码babel转化AST源码
AST
抽象语法树是babel
插件的核心概念,在编写自定义babel
插件也会用到,因为在代码转换其实就是针对AST
语法树各个节点进行的操作
下边推荐一个在线生成AST语法树工具
生成的AST太长,这里不展示了,有兴趣的可以在线尝试。
AST
树,顾名思义数据结构中典型的一种数据类型-树,那么我们也知道,树都有一个根节点,也会有许多子节点。AST
语法树是会有一个type
值是Program
的根节点,如下
{
"type": "Program",
"start": 0,
"end": 29,
"body": [],
"sourceType": "module"
}
经过观察子节点,其实子节点(包括根节点)都有相同的数据结构,如下
{
"type": "VariableDeclaration",
"start": 0,
"end": 13,
"declarations": [...],
"kind": "let"
}
{
type: "Identifier",
name: ...
}
{
type: "BinaryExpression",
operator: ...,
left: {...},
right: {...}
}
以上只是列举了几个不同类型的节点(注意:出于简化的目的移除了某些属性),其实AST
语法树就是由这些节点组成的,它们组合在一起可以描述用于静态分析的程序语法。
从上边可以得出结论:每一个节点都有一个type
字段代表节点的类型,还定义了一些附加属性用来进一步描述该节点类型。
babel编译
babel编译流程代码演示
上边我们也给出了babel编译代码的流程图,下边我们具体实践一下babel编译流程
这里先简单创建一个空项目,步骤如下:
创建一个文件夹,使用npm init -y
创建package.json
然后在项目下创建src/index.js
文件
let name = "hello babel";
console.log(name);
在package.json
中添加执行scripts
"scripts": {
"build": "node src/index.js"
}
为方便我们后边打断点debug,这里我们利用vscode
工具给我们生成一个launch.js
文件,添加自己的launch配置
我的launch.js内容如下
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Debug",
"runtimeExecutable": "npm",
"restart": true,
"console": "integratedTerminal",
"runtimeArgs": ["run-script", "build"],
}
]
}
具体配置请小伙伴们搜一下...
然后我们点想要断点的地方打上断点,击上图debug按钮运行即可,如下
更多关于vscode调试工具请自行学习,这里不做过多讲述
接下来正式回到babel编译正题,我们需要安装3个babel官方提供的插件
npm install -D @babel/parser @babel/generator @babel/traverse
接下来了解一下这3个包的简单用法,修改src/index.js
代码如下
const Parser = require("@babel/parser")
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;
// 源代码
const compilerCode = `
let age = 10;
age = age + 20;
`
// 源代码经过parse过程(词法分析/语法分析)转换成AST语法树
const ast = Parser.parse(compilerCode, {});
// 对AST语法树上的节点进行操作
traverse(ast);
// ast语法树生成最终代码
const codeObj = generator(ast, {}, compilerCode);
console.log(codeObj.code);
以上只是简单的用代码形式演示了babel是如何编译代码的。
编译生成的代码如下
let age = 10;
age = age + 20;
这里和源代码比较一下发现没有什么差别,因为我们没有使用插件对代码进行操作(压缩,混淆,优化等等)
@babel/parser
包的parse
方法传入源代码,进行词法分析合语法分析,最终生成AST抽象语法树
@babel/traverse
包traverse
方法接收AST
抽象语法树并对其进行遍历(深度遍历),在此过程中对节点进行添加、更新及移除等操作。 这是Babel
或是其他编译器中最复杂的过程,同时也是插件将要介入工作的地方,插件部分我们后边在讲
@babel/generator
包generator
方法接收的AST
抽象语法树转换成字符串形式的代码,同时还会创建源码映射(sourceMap
,根据传入的参数控制是否生成sourceMap
)
上边也提到了,@babel/traverse
的traverse
转换过程是深度遍历整颗树对节点进行操作,它会访问树中的所有节点。这时候该方法第二个参数就起到作用了。这个参数是一个对象,对象每个属性是一个钩子函数。这个对象的属性值除了支持AST
语法树节点的type
值外,还有enter
,exit
;也就是在遍历每个节点的时候会先进入enter
钩子函数,如果存在该节点对应的钩子函数,还会执行该钩子函数,最后在访问该节点结束的时候执行exit
钩子函数...
修改转换代码如下:
traverse(ast, {
enter(path){
console.log(path.type, "-进入")
},
exit(path){
console.log(path.type,"-离开")
}
});
再次debug
运行代码
Program -进入
VariableDeclaration -进入
VariableDeclarator -进入
Identifier -进入
Identifier -离开
NumericLiteral -进入
NumericLiteral -离开
VariableDeclarator -离开
VariableDeclaration -离开
ExpressionStatement -进入
AssignmentExpression -进入
Identifier -进入
Identifier -离开
BinaryExpression -进入
Identifier -进入
Identifier -离开
NumericLiteral -进入
NumericLiteral -离开
BinaryExpression -离开
AssignmentExpression -离开
ExpressionStatement -离开
Program -离开
从上边打印结果可以看出,遍历到每个节点时都有执行enter
,exit
函数。合AST抽象语法树对比,也能看出确实属于深度优先递归遍历
接下来我们在添加VariableDeclaration
钩子函数代码如下,
traverse(ast, {
enter(path){
},
VariableDeclaration(path){
console.log(path.type)
},
exit(path){
}
});
再次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
插件。