小白入门:Babel插件编写

978 阅读5分钟

Babel有3个重要的步骤:解析(parse), 转换(transform), 生成(generate). 其中解析是把代码转成AST,所以咱们要先了解AST.

AST入门

定义:抽象语法树或者称为语法树,是用抽象语句的树结构来描述编程语言的源代码。树中的每个节点代表了源码中出现的一个结构。--维基百科

AST explorer是一个很棒的转换显示AST的在线网站,下面的例子都在这里运行

function add(a,b){
    return a+b
}

上面的代码可以转换成下面的树结构:

{
  "type": "Program",
  "start": 0,
  "end": 214,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 179,
      "end": 214,
      "id": {
        "type": "Identifier",
        "start": 188,
        "end": 191,
        "name": "add"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [
        {
          "type": "Identifier",
          "start": 192,
          "end": 193,
          "name": "a"
        },
        {
          "type": "Identifier",
          "start": 194,
          "end": 195,
          "name": "b"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "start": 196,
        "end": 214,
        "body": [
          {
            "type": "ReturnStatement",
            "start": 202,
            "end": 212,
            "argument": {
              "type": "BinaryExpression",
              "start": 209,
              "end": 212,
              "left": {
                "type": "Identifier",
                "start": 209,
                "end": 210,
                "name": "a"
              },
              "operator": "+",
              "right": {
                "type": "Identifier",
                "start": 211,
                "end": 212,
                "name": "b"
              }
            }
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

虽然上面的JSON结构很长,但是把每个结构折叠起来看,就会发现基本上都是类似的。

{
    "type":"FunctionDeclaration",
    "start": 179,
    "end": 214,
    "id":{...}
}
{
    "type":"BinaryExpression",
    "start": 209,
    "end": 212,
    "left":{...},
    "right":{...}
}

像上面的每一层结构也称为节点, 一个AST树可以由成百上千个节点构成。如果用接口(Interface)来表示,如下所示:

interface Node {
    type: string;
    ...
}

type代表节点的类型,常见的有:

keyvalue
FunctionDeclaration函数声明
Identifier标识符
BinaryExpression二进制表达式

除了type,通用的属性还有start,end,loc。当然不同的type类型会附带不同的辅助属性来描述节点信息。

开始编写插件

  1. 第一步要先在.babelrc添加自定义的插件
{
    ...
    "plugins": ["./babel/index"] //你的插件目录位置
}
  1. 新建插件文件,把下面的模板copy进去
module.exports = function ({ types: t }) {
  return {
    visitor: {
      
    }
  };
};

解析:函数会接收当前的babel作为入参,由于会经常用到babel.types,所以可以通过对象解构来获取types({types:t}).在返回的对象中,visitor是这个插件的主要访问者,visitor里面有很多函数,每个函数都会接收两个参数path,state

下面通过一些简单的使用场景来学习编写插件

场景一:去掉console

假设需要在代码中去掉所有的console打印,首先需要了解console的AST结构,可以到AST explorer查看,下面是截图:

WX20210608-090350.png

在例子中可以看到离console.log的最近父节点是MemberExpression,那么就可以在visitor对象里面编写这个函数,如下所示:

visitor:{
    MemberExpression(path){
        ...
    }
}

接下来就要判断里面的语句是否包含console,如果没有,需要提前返回,如何判断呢?有两种方法,一种是直接在path对象中查找,另一种是使用babel自带的方法查找:

//方法一
if(path.node.object.name!=='console') return

//方法二
if (!path.get('object').isIdentifier({ name: 'console' })) return;

找到console语句后,接着就需要移除该语句了,首先移除节点可以使用path.remove方法,但是这里直接使用会报错,我们需要找到属于整条语句的节点,仔细看上面的截图,发现像console.log这种表达式是属于ExpressionStatement,可以通过findParent方法查找父节点,从而删除整个语句。

// 向上查找父节点并删除
const parent = path.findParent((path) => path.isExpressionStatement());
parent.remove();

整体代码如下所示:

module.exports = function ({ types: t }) {
  return {
    visitor: {
      MemberExpression(path) {
        if (!path.get('object').isIdentifier({ name: 'console' })) return;
        const parent = path.findParent((path) => path.isExpressionStatement());
        parent.remove();
      }
    }
  };
};

场景二:替换敏感词

假设有个需求要替换代码中的敏感词,比如下面的代码:

const output = '你居然想白嫖,还想下次一定???';
console.log(output)

首先第一步还是要拿到对应的AST结构,如下图所示,output语句最近的父级节点是VariableDeclarator

WX20210615-105014.png

第二步定义一个黑名单列表,这里选择从插件配置中传入,只需要修改.babelrc文件:

"plugins": [
    ["./babel/index",{
      "blackList":["下次一定","白嫖"]
    }]
  ]

接着在代码中,通过state来获取传入的参数:

VariableDeclarator(path,state){
  const {blackList} = state.opts;
  ...
}

最后就简单了,用正则替换对应的字符串即可,整体代码如下:

VariableDeclarator(path,state){
    if(!path.node.init) return;
    let value = path.node.init.value;
    const {blackList} = state.opts;
    if(!blackList) return
    blackList.forEach(item=>{
      value = value.replace(new RegExp(item,'ig'),'**');
    })
    path.node.init.value = value;
}

场景三:箭头函数转换

下面我们来看看如何把箭头函数转成普通的函数。下面是测试示例:

//简单的箭头函数
const fn = (data)=>{
    console.log(data)
}

其实有个自带的方法arrowFunctionToExpression可以直接转换,看代码:

ArrowFunctionExpression(path){
    if (!path.isArrowFunctionExpression()) return;
    path.arrowFunctionToExpression()
}

嗯,有点简单,感觉不过瘾,要不咱们来手动转换一下。来一个简单的版本吧,先捋一下思路: 获取箭头函数的结构,然后生成一个函数结构替换。 生成函数结构需要用到functionDeclaration方法,该方法一共可以传5个参数,这里重点讲一下前3个参数:第一个是标识id;第二个是参数数组;第三个是主体body,下面一步步来获取所需要的参数

  1. 标识id
// 标识id可以通过父级直接获取id属性
const name = path.parent.id&&path.parent.id.name? path.parent.id.name:'_fn'
  1. 参数数组
//参数可以通过params属性获取,注意获取到的是一个数组,需要遍历取node
const paramsSource = path.get('params')
const params = []
for(let i=0;i<paramsSource.length;i++){
  params.push(paramsSource[i].node)
}
  1. 主体body
//主体可以直接拿箭头函数的body来copy
const body = path.get('body').node

整体代码如下:

ArrowFunctionExpression(path){
    if (!path.isArrowFunctionExpression()) return;
    const name = path.parent.id&&path.parent.id.name? path.parent.id.name:'_fn';
    const paramsSource = path.get('params');
    const params = [];
    for(let i=0;i<paramsSource.length;i++){
      params.push(paramsSource[i].node)
    }
    const node = t.functionDeclaration(t.Identifier(name), params, path.get('body').node);
    const parent = path.findParent((path)=>path.isVariableDeclaration());
    parent.replaceWith(node); //替换箭头函数结构体
}

上面写的转换代码还是比较粗糙的,还有一些地方没有考虑到,比如this指向的问题,如果想深入理解,可以看arrowFunctionToExpression的源码

总结

通过上面几个场景,对babel插件编写进行简单的入门,主要涉及了查找,删除和替换等基本操作。由于本人技术有限,如果有纰漏,请多多包涵和指出问题

参考资料