AST介绍和babel插件开发

·  阅读 249

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,我们关注点在前两步,具体要操作的是第二步。

image.png

parse

将源码解析成ast分为两步,词法分析和语法分析,以 const a = 5 + 3;为例。

词法分析是将源码字符串拆分成一个个token,我们这里把token简化为

interface Token {
 type: string,
 value: string
}
复制代码

我们例子里的结果就是

image.png

语法分析就是将这些token组合成ast

image.png

大概如下

{
    "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

image.png

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'),
            ])
          )
        }
      },
复制代码

参考

分类:
前端
收藏成功!
已添加到「」, 点击更改