为什么要用babel转换代码
我们之前做一些兼容都会都会接触一些 Polyfill 的概念,比如如果某个版本的浏览器不支持 Array.prototype.find 方法,但是我们的代码中有用到 Array 的 find 函数,为了支持这些代码,我们会人为的加一些兼容代码。
if (!Array.prototype.find) {
Object.defineProperty(Array.prototype, 'find', {
// todo someting
})
}
对于这种情况做兼容也很好实现,引入一个 Polyfill 文件就可以了,但是有一些情况我们使用到了一些新语法,或者一些其他写法
// 箭头函数
var a = () => {}
// jsx
var component = () =>
这种情况靠 Polyfill,因为一些浏览器根本就不识别这些代码,这时候就需要把这些代码转换成浏览器识别的代码。 babel就是做这个事情的。
babel做了哪些事情babel做了哪些事情
为了转换我们的代码, babel做了三件事:
Parser 解析我们的代码转换为 AST。
Transformer 利用我们配置好的 plugins/presets把 Parser生成的 AST转变为新的 AST。
Generator 把转换后的 AST生成新的代码
从图上看 Transformer 占了很大一块比重,这个转换过程就是 babel中最复杂的部分,我们平时配置的 plugins/presets就是在这个模块起作用。
从简单的说起
可以看到要想搞懂 babel, 就是去了解上面三个步骤都是在干什么,我们先把比较容易看懂的地方开始了解一下。
Parser 解析
解析步骤接收代码并输出 AST,这其中又包含两个阶段词法分析和语法分析。词法分析阶段把字符串形式的代码转换为 令牌(tokens) 流。语法分析阶段会把一个令牌流转换成 AST 的形式,方便后续操作。
Generator 生成
代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。
babel的核心内容
看起来 babel的主要工作都集中在把解析生成的 AST经过 plugins/presets然后去生成 新的AST这上面了。
AST抽象语法树(AbstractSyntaxTree)
我们想象一下要表示上述代码应该是什么样子,首先必须有东西可以表示这些具体的 声明, 变量, 常量的具体信息,比如
var a = 1 + 2
一个声明语句,声明类型是var,左侧是变量,右侧是表达式。有了这些信息我们就可以还原这个程序,这也是把代码解析成 AST时候所做的事情,对应上面我们说的 词法分析 和 语法分析
AST 在线解析
AST Node节点说明
节点
看这个文档时候我们可以看到说明大多是类似这种
interface Node {
type: string;
loc: SourceLocation | null;
}
这里提到 interface这个我们在其他语言中是比较常见的,比如 Node规定了 type和 loc属性,如果其他节点继承自 Node,那么它也会实现 type和 loc属性就是说继承自 Node的节点也会有这些属性,基本所有节点都继承自 Node,所以我们基本可以看到 loc这个属性 loc表示个一些位置信息
节点遍历
babel拿到抽象语法树后会使用 babel-traverse进行递归的树状遍历,对于每一个节点都会向下遍历到尽头,然后向上遍历退出分支去寻找下一个分支。这样确保我们能找到任何一个节点,也就是能访问到我们代码的任何一个部分。可是我们要怎么去完成修改操作呢, babel给我们提供了下面这两个概念。
如何写一个plugin
visitor
我们已经知道 babel会遍历节点组成的抽象语法树,每一个节点都会有自己对应的 type,比如变量节点 Identifier等。我们需要给 babel提供一个 visitor对象,在这个对象上面我们以这些节点的 type做为 key,已一个函数作为值,类似如下:
const visitor = {
Identifier: {
enter() {
console.log(
'traverse enter a Identifier node!'
)
},
exit() {
console.log(
'traverse exit a Identifier node!'
)
}
}
}
这样在遍历进入到对应到节点时候, babel就会去执行对应的 enter函数,向上遍历退出对应节点时候, babel就会去执行对应的 exit函数,接着上面的代码我们可以做一个测试
const MyVisitor = {
visitor
}
const result = babel.transform(code, {
plugins: [
MyVisitor
]
})
console.log(result.code)
Paths
visitor在遍历到对应节点执行对应函数时候会给我们传入 path参数。它传入的 path参数看起来是这样的:
{
"parent": {
"type": "VariableDeclarator",
"id": {...
},
....
},
"node": {
"type": "Identifier",
"name": "..."
}
}
从上面我们可以看到 path 表示两个节点之间的连接,通过这个对象我们可以访问到节点、父节点以及进行一系列跟节点操作相关的方法。
babel为了方便我们开发,在每一个环节都有很多人性化的定义也提供了很多实用性的工具,比如之前我们在定义 visitor时候分别定义了 enter, exit函数,可很多时候我们其实只用到了一次在 enter的时候做一些处理就行了。所以我们如果我们直接定义节点的 key为函数,就相当于定义了 enter函数。
const visitor = {
Identifier: {
enter(path) {
// todo
}
}
}
// 等同于
const visitor = {
Identifier() {
// todo
}
}
bable-core API参见
bable-types API参见
bable相关核心库与组件