# 写一个Babel插件
# 什么是Babel?
来看一下官方解释:
Babel 是一个 JavaScript 编译器。
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法, 以便能够运行在当前和旧版本的浏览器或其他环境中。
作为一种语言,JavaScript 在不断发展,新的标准/提案和新的特性层出不穷。 在得到广泛普及之前,Babel 能够让你提前(甚至数年)使用它们。
# Babel的原理是什么?
一图胜千言
可以看到Babel主要做了三件事:解析 parse 、转换 transform 、生成 generate 。
# 1. 解析 parse
解析阶段的产物是 abstract syntax tree ,AST抽象语法树。
解析有词法分析和语法分析两个步骤。
- 词法分析
词法分析(英语:lexical analysis)是计算机科学中将字符序列转换为标记(token)序列的过程。
语法分析阶段,把字符串形式的代码转换为tokens,可以理解为一组标记数组。
词法分析类似于我们的分词过程,“我 想吃 火锅”,名词、动词、名词。
比如sum = 2 + 3
,标识符、操作符、数字、操作符、数字。
[
{ type: { ... }, value: "sum", start: 0, end: 2 },
{ type: { ... }, value: "=", start: 3, end: 4 },
{ type: { ... }, value: "2", start: 4, end: 5 },
...
]
Lexical_analysis (opens new window)
- 语法分析
进行语法检查、并构建由输入的单词组成的数据结构
简单点,说话的方式简单点,就是把 tokens 转换成 AST抽象语法树。
# 2. 转换 transform
转换阶段接收一棵AST抽象语法树,对其进行遍历,做一些添加节点、删除节点、修改节点的操作,并输出转换后的AST抽象语法树。
# 3. 生成 generate
这一阶段也很好理解,接收转换好的AST抽象语法树,生成字符串形式的代码,并创建源码映射。
Introduction to JavaScript Source Maps (opens new window)
JavaScript Source Map 详解 (opens new window)
Babel 实际上是一组模块的集合。
babylon 是 Babel 的解析器。
babel-traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点。
babel-generator 模块是 Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)。
babel-types 模块是一个用于 AST 节点的 Lodash 式工具库,它包含了构造、验证以及变换 AST 节点的方法。
# Babel插件是做什么的?
Babel's code transformations are enabled by applying plugins (or presets) to your configuration file.
插件是干什么的呢?
直译一下,Babel的代码转换得益于在配置文件中设置的插件和预设的应用。
# Visitors(访问者)和 Paths(路径)
开始写plugin之前,我们还需要了解一下如何访问语法树的节点,节点与节点之间又是怎样关联的。
# Visitors(访问者)
访问者模式 (opens new window)是一种将数据操作和数据结构分离的设计模式。
简单的说,访问者就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。
看一下官方例子:
const MyVisitor = {
Identifier() {
console.log("Called!");
}
};
这是一个简单的访问者,把它用于遍历中时,每当在树中遇见一个 Identifier 的时候会调用 Identifier() 方法。
function square(n) {
return n * n;
}
这段代码会触发几次"Called!"打印?
可以看一下这段代码解析出来的AST抽象语法树 (opens new window)。
有四个Identifier,所以触发四次。
实际上,Identifier() { ... }
是 Identifier: { enter() { ... } }
的简写形式,一个访问者有两次机会访问节点:进入节点,退出节点。
const MyVisitor = {
"Identifier|FunctionDeclaration|BlockStatement|ReturnStatement|BinaryExpression": {
enter(path) {
console.log("Entered!",path.node.type,path.node.name || '');
},
exit(path) {
console.log("Exited!",path.node.type,path.node.name || '');
}
}
};
可以把方法名用"|"分割成"Idenfifier|MemberExpression"形式的字符串,把同一个函数应用到多种访问节点。
# Paths(路径)
Path 是表示两个节点之间连接的对象。
当我们通过Visitor来访问节点时,实际访问当不是节点,而是路径。
const MyVisitor = {
"Identifier": {
enter(path) {
if(path.node.name === 'a') console.log(path);
},
}
}
用上面这个访问者访问var a = 1
的AST抽象语法树,我们可以看到此时的path:
路径对象还包含添加、更新、移动和删除节点有关的其他很多方法。
当然path中还有很多其他信息,有兴趣可以去了解一下
{
"parent": {...},
"node": {...},
"hub": {...},
"contexts": [],
"data": {},
"shouldSkip": false,
"shouldStop": false,
"removed": false,
"state": null,
"opts": null,
"skipKeys": null,
"parentPath": null,
"context": null,
"container": null,
"listKey": null,
"inList": false,
"parentKey": null,
"key": null,
"scope": null,
"type": null,
"typeAnnotation": null
}
# 写一个Babel插件吧
来写一个删除console.log(...)
的插件吧。
plugin 是一个接收了当前babel对象作为参数的 function,我们先把babel.types
取出来,我们将用到它的一些验证节点类型的方法。
module.exports = function({ types: t }) {
return {
visitor: {
// ...
}
}
}
来看看一条console.log(...)
语句的AST抽象语法树 (opens new window)吧。
然后就可以完成这个插件了!
CallExpression(path, state) {
let node = path.node.callee
if(t.isMemberExpression(node)
&& t.isIdentifier(node.object)
&& node.object.name === 'console'
&& t.isIdentifier(node.property)
&& node.property.name === 'log' ){
path.parentPath.remove();
}
},
# 一个小知识
- 插件的执行顺序是怎样的?
插件顺序从前往后排列。
- 预设的执行顺序是怎样的?
Preset 顺序是颠倒的(从后往前)。
- 插件和预设的执行顺序是怎样的?
插件在 Presets 前运行。