深入Babel原理系列(一)Babel工作流程与项目结构简介

最近接触了一点关于Babel的知识,产生了一些兴趣,于是就打算看一看Babel的原理,然后总结学习下,这东西太复杂了,就分多个博客来写吧,这篇博客主要讲两件事,第一,简单描述下Babel的工作流程,第二,简单介绍下Babel的项目结构,也就是微内核模式。

工作流程

这里提前提一下,图中Traverser调用多个Transformer的这个结构就是微内核。

也就是说其实Babel的核心代码只包括左边那一列,Parser已经内置支持很多语法. 例如 JSX、Typescript、Flow、以及最新的ECMAScript规范。目前为了执行效率,parser是不支持扩展的

其他的附加功能,都是一个个Transformer通过插件的形式实现的,只是Babel实现了一些内置的Transformer去实现一些常用的功能,如转换es2015+代码。

解析(Tokenizer + Parser)

对于源码,此时我们就把它看出一个字符串,对其分析的第一步,肯定是先把源码转换成AST,才好后续操作。

有一个在线AST转换器,我们在这上面可以做实验,写出的代码,它就帮我们翻译成AST:

我什么都不写,AST就有一个根结点了:

1
2
3
4
5
6
7
8
// AST
{
"type": "Program",
"start": 0,
"end": 0,
"body": [],
"sourceType": "module"
} // 可以看成是一个对象,有一些字段,这代码树的根结点。

然后我在写一句const text = 'Hello World'; ,就变成了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"type": "Program",
"start": 0,
"end": 27,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 27,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 26,
"id": {
"type": "Identifier",
"start": 6,
"end": 10,
"name": "text"
},
"init": {
"type": "Literal",
"start": 13,
"end": 26,
"value": "Hello World",
"raw": "'Hello World'"
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}

从这个结构我们可以简单看一下AST节点的结构。

每个节点都有type,start和end,type表明了节点的类型,就像根节点的类型就是Program,就是所有的代码,所以它的start是0,end是最后;

而不同类型的节点可能会有自己不同的定义,如VariableDeclaration节点就有kind属性,表示是通过const还是var,let来声明变量的, declarations属性,表示具体的内容,它是个数组,也就是说一个VariableDeclaration可以声明多个节点,每个节点的init属性表示该变量初始化的数据是什么。

总结AST树的特点:

  1. 节点是有类型的。我们学习树这种数据结构时,节点都是最简单的,这里复杂了,有类型。
  2. 节点与子节点的关系,是通过节点的属性链接的。我们学习的树结构,都是left、right左孩子右孩子的。但是AST树,不同类型的节点,属性不同,Program类型节点的子节点是它的body属性,VariableDeclaration类型的子节点,是它的declarations、kind属性。也就是节点的属性看作是节点的子节点,并且子节点也可能有类型,近而形成一个树。
  3. 父节点是所有子节点的组合,我们可以看到VariableDeclaration代表的const text = 'Hello World’被拆分成了下面两个子节点,子节点又继续拆分。

希望能从上面的分析中,让大家对AST有一个最直观的认识,就是节点有类型的树。

那么节点的类型系统就很必要了解了,这里是Babel的AST类型系统说明。大家可以看看,可以说类型系统是抽象了代码的各种成员,标识符、字面量、声明、表达式。所以拥有这些类型的节点的树结构,可以用来表达我们的代码。

Travese

第二步:转换。得到ast了,该操作它了,Babel中的babel-traverse用来干这个事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 安装
npm install --save babel-traverse

// 实验代码
import * as babylon from "babylon";
import traverse from "babel-traverse";

const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);

traverse(ast, {
enter(path) {
console.log('path', path);
}
})

console.log('ast', ast);

babel-traverse库暴露了traverse方法,第一个参数是ast,第二个参数是一个对象,我们写了一个enter方法,方法的参数是个path,咋不是个node呢?我们看一下输出:

其实这个path中包含了node属性,同时也包含了很多用于其他分析的属性,如分析作用域的scope属性等。

生成器 babel-generator

第三步:生成。得到操作后的ast,该生成新代码了。Babel中的babel-generator用来干这个事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
npm install --save babel-generator

// 加入babel-generator
import * as babylon from "babylon";
import traverse from "babel-traverse";
import * as t from "babel-types";
import generate from "babel-generator";

const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);

traverse(ast, {
enter(path) {
if (t.isIdentifier(path.node, { name: "text" })) {
path.node.name = 'alteredText';
}
}
})

const genCode = generate(ast, {}, code);

console.log('genCode', genCode);

微内核与插件

上面我们讲过了Babel的工作流程,我们发现Babel的核心功能不大,很小其实是分四步走,把代码拆分为token,把token序列构建成AST,对AST进行一些操作,最后再把处理后的AST转化为新的代码。

这个核心功能不大,但是又为了能支持复杂的功能,所以在第三步对AST的处理提供了插件机制(这个插件机制是通过访问者模式实现的),而这种架构方式就叫做微内核。

具体的解释可以看这篇博客:https://bobi.ink/2019/10/01/babel/#访问者模式

参考链接:

https://juejin.cn/post/6844903905961181191

https://www.babeljs.cn/docs/