babel是个js编译器,写babel插件其实就是操作ast。
我们日常项目中使用ast的场景很多,比如eslint, codemods, css parsers, css in js等等,不同的工具的ast解析规则可能稍有区别,这里以babel的规范为主。
ast介绍
ast的定义如下
An abstract syntax tree is a tree representation of source code written in a programming language. Each node of the tree denotes a construct occurring in the source code.
ast的处理包括三个步骤, parse, transform, generate,我们关注点在前两步,具体要操作的是第二步。
parse
将源码解析成ast分为两步,词法分析和语法分析,以 const a = 5 + 3;
为例。
词法分析是将源码字符串拆分成一个个token,我们这里把token简化为
interface Token {
type: string,
value: string
}
我们例子里的结果就是
语法分析就是将这些token组合成ast
大概如下
{
"type": "VariableDeclaration",
"declarations": [{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "BinaryExpression",
"left": {
"type": "Literal",
"value": 5
},
"operator": "+",
"right": {
"type": "Literal",
"value": 3
}
}
}],
"kind": "const"
}
具体的ast可以在这里查看astexplorer
ast就像一个DOM tree,每个ast的节点是个Node实例,每个Node有以下接口
interface Node {
type: string;
}
具体的类型还有其他属性,比如Function
interface Function extends Node {
id: Identifier | null;
params: [ Pattern ];
body: BlockStatement;
generator: boolean;
async: boolean;
}
Transform
就是遍历ast,然后对节点进行增删改查,这是我们开发babel插件时接入的阶段,后面会具体介绍。
Generate
就是深度优先遍历ast,生成对应的源码即可,同时还可以生成source map.
写插件
babel插件就是一个函数,返回一个带visitor属性的对象,会在遍历ast过程中对指定类型的节点进行操作,其中t表示type,可以用于创建或验证节点等,具体用法可以参考@babel/types,这块参考的这里。
export default function({ types: t }) {
return {
visitor: {
Identifier(path, state) {},
}
};
};
其中每个节点节点类型对应的回调表示对这个类型节点的操作
path
其中path指的是两个节点的连接,其中可以访问节点信息,也包含相关增删改查方法,类似于DOM操作,其中path上的操作是操作子节点,想操作父节点可以使用path.parentPath
增
插入相邻节点
FunctionDeclaration(path) {
path.insertBefore(t.expressionStatement(t.stringLiteral("Because I'm easy come, easy go.")));
path.insertAfter(t.expressionStatement(t.stringLiteral("A little high, little low.")));
}
+ "Because I'm easy come, easy go.";
function square(n) {
return n * n;
}
+ "A little high, little low.";
插入进一个数组类型节点
ClassMethod(path) {
path.get('body').unshiftContainer('body', t.expressionStatement(t.stringLiteral('before')));
path.get('body').pushContainer('body', t.expressionStatement(t.stringLiteral('after')));
}
class A {
constructor() {
+ "before"
var a = 'middle';
+ "after"
}
}
删
FunctionDeclaration(path) {
path.remove();
}
- function square(n) {
- return n * n;
- }
改
BinaryExpression(path) {
path.replaceWith(
t.binaryExpression("**", path.node.left, t.numberLiteral(2))
);
}
function square(n) {
- return n * n;
+ return n ** 2;
}
查
可以通过.node获取
BinaryExpression(path) {
path.node.left;
path.node.right;
path.node.operator;
}
也可以.get
BinaryExpression(path) {
path.get('left');
}
state
可以用来获取babel配置时的参数
//配置
{
plugins: [
["my-plugin", {
"option1": true,
"option2": false
}]
]
}
结果
export default function({ types: t }) {
return {
visitor: {
FunctionDeclaration(path, state) {
console.log(state.opts);
// { option1: true, option2: false }
}
}
}
}
例子
上篇文章我们说在使用微前端时,import一个线上模块会造成fast refresh失效,因此我们的解决方式是写一个loader,即实现的功能是将
//a指的是线上模块
import('a')
修改为
//b指的是线下模块
import('b')
关键代码为
CallExpression(path, state) {
if (!Array.isArray(state.opts.modules)) {
throw new Error('参数错误')
}
if (
path.node.callee.type === 'Import' &&
//modules参数为需要替换的模块
state.opts.modules.some(
(item) => path.node.arguments[0].value === item
)
) {
path.replaceWith(
t.CallExpression(t.identifier('import'), [
//b为替换成的本地模块
t.stringLiteral('b'),
])
)
}
},