一、babel介绍
Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做“转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。在这个源码到源码的转换过程当中,抽象语法树,即AST,起到了重要的作用。
二、抽象语法树(AST)
抽象语法树(Abstract Syntax Tree)也称为AST语法树,指的是源代码语法所对应的树状结构。也就是说,一种编程语言的源代码,通过构建语法树的形式将源代码中的语句映射到树中的每一个节点上。和我们的常说的虚拟DOM有点像,虚拟dom是用json的格式把DOM结构抽象成js对象,用于描述DOM的结构,每个节点的类型、属性等等,类似的AST是把js的源代码抽象为js对象的格式,以方便描述这段代码的语法。程序代码本身被映射成为一棵语法树,通过操纵语法树,我们能够精准的获得程序代码中的每一个精确的节点。例如声明语句,赋值语句等。
三、babel的处理步骤
Babel 的三个主要处理步骤分别是: 解析(parse),转换(transform),生成(generate)。
解析步骤接收代码并输出 AST
转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程,同时也是插件将要介入工作的部分。在babel-loader中有两种方式可以配置babel插件,我们经常会配置:
rules: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ["env", 'stage-0'],
plugins: [
["extract", { "library": "lodash" }],
["transform-runtime", {}]
]
},
}
]
注意:plugins 的插件使用顺序是顺序的,而 preset 则是逆序的。所以上面的执行方式是extract>transform-runtime>stage-0>env
代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。代码生成比较简单,就是深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。
有了以上的基础知识之后我们看看babel插件是怎么编写的。
四、编写一个简单的babel插件
1、插件格式
先从一个接收了当前babel对象作为参数的function开始。
export default function(babel) {
// plugin contents
}
我们经常会这样写:
export default function({ types: t }) {
//
}
接着返回一个对象,其 visitor 属性是这个插件的主要访问者。
export default function({ types: t }) {
return {
visitor: {
// visitor contents
}
};
};
Visitor 中的每个函数接收2个参数:path 和 state;
export default function({ types: t }) {
return {
visitor: {
Identifier(path, state) {},
ASTNodeTypeHere(path, state) {}
}
};
};
Path 是表示两个节点之间连接的对象。表示AST中节点之间的相互关联关系。例如,如果有下面这样一个节点及其子节点︰
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
...
}
将子节点 Identifier 表示为一个路径(Path)的话,看起来是这样的:
{
"parent": {
"type": "FunctionDeclaration",
"id": {...},
....
},
"node": {
"type": "Identifier",
"name": "square"
}
}
我们发现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
}
2、写一个简单的插件
我们写一个简单的插件,把所有定义变量名为a的换成b, 先从astexplorer看下var a = 1的 AST
{
"type": "Program",
"start": 0,
"end": 10,
"body": [
{
"type": "variableDeclaration",
"start": 0,
"end": 9,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 9,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "a"
},
"init": {
"type": "Literal",
"start": 8,
"end": 9,
"value": 1,
"raw": "1"
}
}
],
"kind": "var"
}
],
"sourceType": "module"
}
从上面可以看出我们要找的节点类型是VariableDeclarator,以下是我们写出的插件代码:
export default function({ types: t }) {
return {
visitor: {
VariableDeclarator(path, state) {
if (path.node.id.name == 'a') {
path.node.id = t.identifier('b')
}
}
}
}
}
我们要把id属性是 a 的替换成 b 就好了。但是这里不能直接path.node.id.name = 'b'。如果操作的是object,就没问题,但是这里是 AST 语法树,所以想改变某个值,就是用对应的 AST 来替换,现在我们用新的标识符来替换这个属性。
我们用@babel/core中的transform方法对我们写的插件进行测试一下:
import * as babel from '@babel/core';
const c = `var a = 1`;
const { code } = babel.transform(c, {
plugins: [
function({ types: t }) {
return {
visitor: {
VariableDeclarator(path) {
if (path.node.id.name == 'a') {
path.node.id = t.identifier('b')
}
}
}
}
}
]
})
console.log(code); // var b =
3、实现一个简单的按需打包的功能
例如我们要实现把import { Button } from 'antd'转成import Button from 'antd/lib/button'
通过对比 AST 发现,specifiers里的type和source不同。
// import { Button } from 'antd'
"specifiers": [
{
"type": "ImportSpecifier",
...
}
],
"source": {
"type": "Literal",
"start": 23,
"end": 29,
"value": "antd",
"raw": "'antd'"
}
// import Button from 'antd/lib/button'
"specifiers": [
{
"type": "ImportDefaultSpecifier",
...
}
],
"source": {
"type": "Literal",
"start": 19,
"end": 36,
"value": "antd/lib/button",
"raw": "'antd/lib/button'"
}
import * as babel from '@babel/core';
const c = `import { Button } from 'antd'`;
const { code } = babel.transform(c, {
plugins: [
function({ types: t }) {
return {
visitor: {
ImportDeclaration(path) {
const { node: { specifiers, source } } = path;
if (!t.isImportDefaultSpecifier(specifiers[0])) { // 对 specifiers 进行判断
const newImport = specifiers.map(specifier => (
t.importDeclaration(
[t.ImportDefaultSpecifier(specifier.local)],
t.stringLiteral(`${source.value}/lib/${specifier.local.name}`)
)
))
path.replaceWithMultiple(newImport)
}
}
}
}
}
]
})
console.log(code); // import Button from "antd/lib/Button"
五、总结
我们介绍了babel的功能、babel工作的三个阶段以及怎么写一个简单的插件,写插件主要用到了transform这个函数,插件的的函数返回一个对象,visitor是这个插件的访问者,通过它来访问AST,在visitor里定义一些函数做相关的操作。如果想了解更多有关babel插件的知识可访问babel插件手册