Babel 是一个 JavaScript 编译器。node
// Babel 输入: ES2015 箭头函数
[1, 2, 3].map((n) => n + 1);
// Babel 输出: ES5 语法实现的同等功能
[1, 2, 3].map(function(n) {
return n + 1;
});
复制代码
Babel经过转换,让咱们写新版本的语法,转换到低版本,这样就能够在只支持低版本语法的浏览器里运行了。git
Babel真厉害,它竟然‘认识’代码、更改代码。那Babel就是操做代码的代码,酷。github
学习Babel对咱们能力的提高有很大帮助,咱们平时都是用代码来操做各类东西,此次咱们操做的对象,变成了代码自己。算法
这个认识和操做代码的过程,学名叫作代码的静态分析。express
先来看一个问题,编辑器里代码的高亮,以下:npm
编辑器能够把代码不一样的成员,标记为不一样的颜色。显然编辑器要‘认识’代码,对代码进行分析和处理,才能达到这种效果。这就叫代码的静态分析。浏览器
静态分析是在不须要执行代码的前提下对代码进行分析的处理过程。bash
动态分析是在代码的运行过程当中对代码进行分析和处理。上面的代码高亮属于静态分析,在代码没有运行的状况下,进行分析和处理的。babel
静态分析不光能高亮代码,还有就是代码转换,还能够对咱们的源代码进行优化、压缩等操做。数据结构
在对代码静态分析的过程当中,要将源码转换成AST (抽象语法树)。
源代码对于Babel来讲,就是一个字符串。Babel要对这个字符串进行分析。咱们平时对字符串的操做,就是使用字符串方法或是正则,但相对字符串(源码)进行复杂的操做,远远不够。
须要将字符串(源码)转换成树的数据结构,才好操做。这个树结构,就叫AST(抽象语法树)。
这里我想到 程序 = 数据结构 + 算法。咱们平时写的业务需求,对数据结构要求不高,简单的对象和列表就能够搞定,但要某些特定的复杂问题,好比如今研究的操做代码,就需先思考:我应该把操做的事物放到什么样的数据结构上,才更容易我写算法/逻辑。
对于源码,此时咱们就把它看出一个字符串,对其分析的第一步,确定是先把源码转换成AST,才好后续操做。
有一个在线AST转换器,咱们在这上面能够作实验,写出的代码,它就帮咱们翻译成AST:
我什么都不写,AST就有一个根结点了:
// AST
{
"type": "Program",
"start": 0,
"end": 0,
"body": [],
"sourceType": "module"
} // 能够当作是一个对象,有一些字段,这代码树的根结点。
复制代码
而后我写一句代码:
// 源码
const text = 'Hello World';
// AST
{
"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"
}
复制代码
懵逼,一句const text = 'Hello World'; 就生成这么多东西。看懂它、理解AST是学习Babel的第一个门槛。
看下图,这就是AST Explorer的界面,左边写代码,右边就帮助咱们翻译成AST,AST有两种表达方式,Tree和JSON,上面都是用JSON形式表示AST,后来我发现仍是用Tree的形式看更容易些,由于Tree的形式更突出节点的类型:
我将AST表达的树画出来,以下:
总结AST树的特色:
但愿能从上面的分析中,让你们对AST有一个最直观的认识,就是节点有类型的树。
那么节点的类型系统就很必要了解了,这里是Babel的AST类型系统说明。你们能够看看,能够说类型系统是抽象了代码的各类成员,标识符、字面量、声明、表达式。因此拥有这些类型的节点的树结构,能够用来表达咱们的代码。
参照类型系统,多实验,咱们就会对AST的结构大致掌握和理解了。
额外提一下,V8中也用到了AST。V8引擎有四个主要模块:
能够看到AST也是V8执行的关键一环。下面下来看看Babel对于AST的利用,及运行步骤。
回看Babel的处理过程
第一步:解析,Babel中的解析器是babylon。咱们来体验一下:
// 安装
npm install --save babylon
复制代码
// 实验代码
import * as babylon from "babylon";
const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);
console.log('ast', ast);
复制代码
code变量是咱们的源代码,ast变量是AST,咱们看一下打印结果:
和咱们预期的同样,获得AST了。这里我注意到还有start、end、loc这样位置信息的字段,应该能够对生成Source Map有用的字段。
第二步:转换。获得ast了,该操做它了,Babel中的babel-traverse用来干这个事。
// 安装
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被打印了5次,ast上确实也是有5个节点,是对应的。traverse方法是一个遍历方法,path封装了每个节点,而且还提供容器container,做用域scope这样的字段。提供个更多关于节点的相关的信息,让咱们更好的操做节点。
咱们来作一个变量重命名操做:
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) {
if (path.node.type === "Identifier"
&& path.node.name === 'text') {
path.node.name = 'alteredText';
}
}
})
console.log('ast', ast);
复制代码
看结果:
确实咱们的ast被更改了,用这个ast生成的code就会是const alteredText = 'Hello World';
在利用babel-traverse操做AST时,也能够利用工具库帮助咱们写出更加简洁有效的代码,就可使用babel-types。
npm install --save babel-types
复制代码
import * as babylon from "babylon";
import traverse from "babel-traverse";
import * as t from "babel-types";
const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);
traverse(ast, {
enter(path) {
// 使用babel-types
if (t.isIdentifier(path.node, { name: "text" })) {
path.node.name = 'alteredText';
}
}
})
console.log('ast', ast);
复制代码
使用babel-types实现和上例中同样的功能,代码量更少了。
第三步:生成。获得操做后的ast,该生成新代码了。Babel中的babel-generator用来干这个事。
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);
复制代码
来看打印结果:
nice! 在code字段里,咱们看到里新生成的代码。
固然,上面提到的这四个库,还有更多细节,有兴趣的能够再研究研究。
这些库的集合,就是咱们的Babel。
咱们研究过了Babel的内部,如今跳出来,咱们须要在外部操做AST,而不是进Babel内部去改traverse。
从外部操做AST,就须要插件了。咱们来研究一个插件babel-plugin-transform-member-expression-literals
使用次插件后:
// 转换前
obj.const = "isKeyword";
// 转换后
obj["const"] = "isKeyword";
复制代码
咱们再来看看这个插件的源码:
{
name: "transform-member-expression-literals",
visitor: {
MemberExpression: {
exit({ node }) {
const prop = node.property;
if (
!node.computed &&
t.isIdentifier(prop) &&
!t.isValidES3Identifier(prop.name)
) {
// foo.default -> foo["default"]
node.property = t.stringLiteral(prop.name);
node.computed = true;
}
},
},
},
};
}
复制代码
这里我尝试将它放到咱们的实验代码里,以下:
import * as babylon from "babylon";
import traverse from "babel-traverse";
import * as t from "babel-types";
import generate from "babel-generator";
const code = `const obj = {};obj.const = "isKeyword";`;
const ast = babylon.parse(code);
const plugin = {
MemberExpression: {
exit({ node }) {
const prop = node.property;
console.log('node', node);
if (
!node.computed &&
t.isIdentifier(prop)
// !t.isValidES3Identifier(prop.name) 这里注释掉,咱们的t里没这个方法
) {
// foo.default -> foo["default"]
node.property = t.stringLiteral(prop.name);
node.computed = true;
}
},
},
};
traverse(ast, plugin)
const genCode = generate(ast, {}, code);
console.log('genCode', genCode);
复制代码
输出的代码是"const obj = {};obj["const"] = "isKeyword";"。符合预期,也就是说,Babel的插件,会传给内部的traverse方法。而且是一种符合访问者模式的,让咱们能够针对节点类型(如这里的visitor.MemberExpression)的操做。这里用的是exit,而不是enter了,解释一下,traverse是对树的深度遍历,向下遍历这棵树咱们进入(entry)每一个节点,向上遍历回去时咱们退出(exit)每一个节点。就是对于AST,traverse遍历了两遍,咱们能够选择在进入仍是退出的时候,操纵节点。
个人参考:
今天的研究就到这里,理解Babel内部机制和基本的插件工做方式。