Babel HelloWorld

由于一个偶然的契机,笔者接触到了一个 js 处理神器 ———— babel,可以用来方便快速的处理 js 代码,实现自定义功能。笔者花了几天的时间对 babel 的实现原理和使用方法进行了简单的梳理,特此记录,以备之后的学习。

babel 的官方网站上有着较为完备的文档,其 GitHub 上也有各个语言版本的详细使用手册,笔者在学习过程中主要参考了以下两个网站。

https://www.sitepoint.com/understanding-asts-building-babel-plugin/

https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md#toc-stages-of-babel

基础

Babel 是一个用于操作 js 的框架,它为用户提供了方便的接口可以通过插件自定义操作。

Babel 的主要思想就是通过操作 js 代码对应的 AST 树得到另一个符合语法的 js。

Babel 很典型的分为了以下几个模块

  • babylon,一个从Acorn项目fork出来的解析器 parser

  • traverse。 Babel 的便利和操作模块,用来遍历 ast 和操作 ast,有点类似于操作 DOM 对象的 DOMWriter

  • types 相当于一个语法库,与自己实现的 node 类似,包含了所有的基本语法。一个典型的节点定义如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    defineType("BinaryExpression", {
    builder: ["operator", "left", "right"],
    fields: {
    operator: {
    validate: assertValueType("string")
    },
    left: {
    validate: assertNodeType("Expression")
    },
    right: {
    validate: assertNodeType("Expression")
    }
    },
    visitor: ["left", "right"],
    aliases: ["Binary", "Expression"]
    });
    • Definitions , types 模块中对于节点类型的定义
    • Builder, types 模块中各类型节点的构造器
    • Validators, types 模块中对于节点各个字段的规范,规范有两种形式
      • 第一种是 isX
      • 第二种是 assertX
  • generator , 生成器模块,根据 ast 生成代码

  • template, 模版模块,类似模版字符,可以作为模版生成字符串

遍历

Babel 提供了插件接口,用户可以根据自己的需求开发不同功能的插件

插件是一个独立的 js 模块,导出一个函数,接受 babel 作为参数。为了调用方便通常写成这样的形式

1
2
3
4
5
6
7
8
export default function({ types: t }) {
return {
visitor: {
// visitor contents
}
};
};
// 使用对象结构赋值直接获取 babel.type

Babel 使用访问者模式设计开发,插件在其中就担当访问者。插件最后的返回值也就是一个 visitor 对象,访问者对象中包含一系列函数,可以在访问到这个制定节点前后进行调用。

Babel 的一个重要功能就是用来遍历 AST 和操作替换其中的节点。也就是 traverse 模块主要完成的功能。

1
2
3
4
5
6
7
visitor: {
BinaryExpression(path) {
if (path.node.operator !== "===") {
return;
}
}
}

数据结构

这里涉及两个个关键的数据结构 path 和 state。

节点与节点之间相互链接形成一棵树,作为数据表示,树结构上不宜出现过多的信息,因此使用 path 结构表示当前节点与其他节点之间的关系(这里 github 文档中的说明好像有些问题)

一个 path 的结构大概如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"parent": {...}, // 父节点
"node": {...}, // 当前节点
"hub": {...}, //
"contexts": [],
"data": {},
"shouldSkip": false,
"shouldStop": false,
"removed": false,
"state": null,
"opts": null, // 注册在这个 path 上的观察者
"skipKeys": null,
"parentPath": null, // 父节点的 path
"context": null,
"container": null, // 当前节点所在的容器节点
"listKey": null,
"inList": false,
"parentKey": null,
"key": null,
"scope": null, // scope 结构,表示节点的作用空间
"type": null,
"typeAnnotation": null
}

还包括其他和插入删除相关的函数。

用一个例子可以看出这里的关系。

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
36
37
38
39
40
41
42
43
44
45
46
function squre(n){
return n*n;
}
==》
{
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "squre"
},
"params": [
{
"type": "Identifier",
"name": "n"
}
],
"body": {
"type": "BlockStatement",
"body": [
{
"type": "ReturnStatement",
"argument": {
"type": "BinaryExpression",
"left": {
"type": "Identifier",
"name": "n"
},
"operator": "*",
"right": {
"type": "Identifier",
"name": "n"
}
}
}
]
}
}
],
"sourceType": "module"
}

这段代码生成的 AST 中会有四个 Identifier 节点,分别是函数名,函数参数,函数体中的两处引用。

查看第一个 Identifier 节点的 path,

1
2
3
4
5
6
Path{
parent: FunctionDeclaration
parentPath: FunctionDeclarationPath
container: FunctionDeclaration
scope: FunctionDeclarationScope
}

第二个 Identifier

1
2
3
4
5
6
Path{
parent: FunctionDeclaration
parentPath: FunctionDeclarationPath
container: Identifer // self
scope: FunctionDeclarationScope
}

第三个

1
2
3
4
5
6
Path{
parent: BinaryExpression
parentPath: BinaryExpressionPath
container: BinaryExpression
scope: FunctionDeclarationScope
}

visitor 注册的函数实际接受的参数就是节点对应的 path。

state 可以理解成一个全局变量,用于维护一些可以在回调函数之间传递的信息。默认情况下 state 中保存的是文件信息

scope 用来指示变量的作用域信息,js 语言使用字典式作用域或者叫做静态作用域#Lexical_scoping_vs._dynamic_scoping)。
即外部作用域无法访问内部作用域的内容,内部可以访问外部。当创建一个引用时,这个引用就属于其当前的 scope,当然它也属于当前 scope 的上层 scope。

scope 在对AST 节点做变更时十分有用。它可以保证新增引用的正确性。也可以找出一个变量在什么地方被引用了

1
2
3
4
5
6
7
8
{
path: path,
block: path.node,
parentBlock: path.parent,
parent: parentScope,
bindings: [...] // 绑定,在当前作用域内声明的引用
globals: // 只有全局 scope 中会存的 全局变量
}

很显然上例中四个 Identifer 的 scope 指向的都是同一个 scope.

bindings 是 scope 中存储的最重要的信息,这里记录了所有声明在这个 scope 中的变量及其引用情况。

举例来说一个 functionNode,其 scope.bindings 中存储的就是这个函数参数以及参数在这个函数内的引用信息。

通过 binding 何以很方便的访问一个变量的所有引用 ,但是这里会有一个问题,即只有取值的时候才会算作是引用,而对于赋值操作则不会记录在 referencePaths 字段,转而存在 constantViolations 字段中

1
2
3
4
5
6
7
8
9
10
// 替换 name
function update_name(path, binding, name){
binding.identifier.name = name;
if(binding.referenced){
for(let i =0; i<binding.references; i++){
binding.referencePaths[i].node.name = name;
}
}
}

操作

为了得到一个AST节点的属性值,我们一般先访问到该节点,然后利用 path.node.property 方法即可。

1
2
3
4
5
BinaryExpression(path) {
path.node.left;
path.node.right;
path.node.operator;
}

有时需要从一个节点向上遍历,可以调用函数 findParent 或者 find ,函数接受一个回调函数作为参数,以便用户控制

1
2
path.findParent((path) => path.isObjectExpression());
path.find((path) => path.isObjectExpression());

除遍历节点外,最常用的 AST 操作是替换节点,此时可以使用函数 repalceWith 或者 replaceWithMultiple。当然也可以使用 replaceWithSourceString 直接用 js 代码字符串替换之(github 手册上的例子会导致问题)

1
2
3
4
5
6
BinaryExpression(path) {
path.replaceWith(
if (path.node.operator === "*")
t.binaryExpression("**", path.node.left, path.node.right)
);
}

使用 insertBefore 、insertAfter、pushContainer、unshiftContainer 来插入节点。调用 remove 来清除节点。

运行

运行 Babel 有多种方式

最直接的一种就是通过命令行直接运行,目前测试时也选用这种方法 node_modules/.bin/babel test.js --plugins=./plugins/firstplugin

如果需要以编程的形式使用 babel 则需要额外安装 npm install babel-core

直接按照 node 的调用规范调用其中的模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var parser = require("@babel/parser");
var traverse = require("@babel/traverse").default;
const code = `function square(n) {
return n * n;
}`;
const ast = parser.parse(code);
traverse(ast, {
enter(path) {
if (path.isIdentifier({ name: "n" })) {
path.node.name = "x";
}
}
});

或者更简单的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
babel = require(@babel/core)
const filename = "example.js";
const code = fs.readFileSync(filename, "utf8");
// Load and compile file normally, but skip code generation.
const { ast } = babel.transformSync(code, { filename, ast: true, code: false });
// Minify the file in a second pass and generate the output code here.
const { code, map } = babel.transformFromAstSync(ast, code, {
filename,
presets: ["minify"],
babelrc: false,
configFile: false,
});

功能

Babel 提供了非常强大的接口,使用这些接口我们可以实现很多非常强大的功能

变量规范化

变量规范化的目的是为了方便的区分函数,(常量)和普通变量

Reference

http://coderlt.coding.me/2017/05/02/babel-readme/

https://www.kancloud.cn/digest/babel/217106

https://juejin.im/entry/59ba1a3c5188255e723b8cae

https://www.h5jun.com/post/babel-for-es6-and-beyond.html