原来babel插件是这样写的

1,581 阅读7分钟

前言

本文重点讲述如何写一个babel插件,对于一些知识概念并没有过多的描述,废话少说啦,直接奔入主题。

写一个这样的插件

比如我们有以下代码:

const a = 1;
const b = 10;
const c = (d) => {
  var e = 20;
  console.log(d);
}

我们要写一个插件,主要是把上面代码 const 转换成 var , 把箭头函数转换成 function,最后再把箭头函数里的形参及函数体内的变量d改成变量m。即转换后代码如下:

var a = 1;
var b = 10;
var c = function (m) {
  var e = 20;
  console.log(m);
};

一些准备知识点

babel是什么

babel可以看作是一个转换器,可以把一些代码转成另外一些代码。其转换过程可以分为3个步骤:

  1. 源码解析生成AST
  2. AST转换成新的AST(babel插件在此过程对代码进行改造)
  3. 新的AST生成新的代码

AST(抽象语法树)

AST是一颗描述代码的树,其中每个节点代表代码中某一个位置的意思,比如通过 astexplorer 将代码const a = 1;转换为AST,可得到如下一棵节点树:

从图中,可以看到每个节点都有一个type字段,type正是代表每个节点是那种类型的节点,AST的type远不止图中列举的三种,更多的type可以到这里 手册 查阅。

babel插件一些关键方法及工具

  1. Visitor

    Visitor对象就类似监听方法的大集合,当babel在处理AST每个节点时,如果在Visitor有声明某个节点类型的方法,当babel处理AST节点的类型和此节点类型方法一致就会执行方法,比如:

    visitor: {
        Identifier(path) { // 当节点类型为identifier,此方法就会执行。
            console.log('type is identifier')
        }
    }
    
  2. Path

    上面的visitor对象声明的方法中,第一个参数是一个名为path参数,这个path参数中包含了节点的信息以及对节点的添加、更新、移动和删除节等方法,更多关于path定义可以直接查看path源码进行查阅。

  3. babel-types

    babel-types用于处理AST节点的工具库,包含了构造、验证AST节点等方法。更多信息可以到babel官网查阅。

开始写插件

上述描述的插件功能可以分为3点:

  • 将const转换为var
  • 将箭头函数转换为function函数
  • 将箭头函数里的形参及函数体内的变量d改成变量m

接下来我们一一来实现,首先是将const转换为var:

  1. 添加visitor对象的节点监听方法

    上文我们提到我们需要在visitor对象里添加我们对应的类型方法,但是有一个问题,要如何知道我们所需要的类型对应方法写法呢?其实这个可以将我们的源码放到astexplorer 转换以下成AST,然后找到对应的type字段,就可以知道我们要在visitor对象里写哪些方法。这里我们将上文提到的要转换源码生成AST:

    var a = 1;
    var b = 10;
    var c = function (m) {
      var e = 20;
      console.log(m);
    };
    

    从AST中可以看到body下面有三个类型为VariableDeclaration的数组元素,而VariableDeclaration类型正是对应变量声明的一种类型,查看下babel-types 手册,看下定义:

    可以看到babel-types里定义了变量的构造方法t.variableDeclaration(kind, declarations),其中参数kind可以取"var" | "let" | "const"

    所以这里我们要在visitor对象添加监听类型方法,便是VariableDeclaration

    visitor: {
      VariableDeclaration(path) {
        console.log(path)
      }
    }
    
  2. 转换AST

    上文我们已经在visitor添加了针对变量的监听方法,而且也知道节点为VariableDeclaration类型有一个变量为kind标识着声明变量的方式(从上文AST中也可以看出来,即kind: "const"),所以我们这里是要将const转换为var,那可以直接在visitor里监听的VariableDeclaration方法将该节点kind赋值为var即可:

    visitor: {
      VariableDeclaration(path) {
        if(path.node.kind === 'const') { // 如果kind为const,则转换为var
            path.node.kind = 'var'
        }
      }
    }
    

    运行下代码即可看到我们所需要的效果:

    上图中我们已经成功将const转换为var,转换方式就是将节点的kind字段直接赋值为var,除了这种方式,我们也可以通过替换当前节点的方式,来达到同样的效果。那如何构造呢,上文说过 “babel-types用于处理AST节点的工具库,包含了构造、验证AST节点等方法” ,所以我们可以直接用babel-types定义的构造方法来构造一个variableDeclaration节点,具体方法在上文已经截图展示了,这里直接上代码:

    visitor: {
      VariableDeclaration(path) {
        if(path.node.kind === 'const') { // 如果kind为const,则转换为var
            // types 为 babel-types工具库
            let node = types.VariableDeclaration('var', path.node.declarations)
            // replaceWith为替换节点的方法
    	path.replaceWith(node);
        }
      }
    }
    

    上述代码也可以达到同样的转换效果,其中replaceWith这个方法为替换节点的方法,可在源码中查看使用方法。

接下来我们要来实现箭头函数转换为function函数功能,同样按照上面两个步骤

  1. 添加visitor对象的节点监听方法

    同样方式,我们还是从AST树上对应的箭头函数类型的监听方法的写法

    从AST中可以看出箭头函数类型是ArrowFunctionExpression,所以visitor里的写法:

    visitor: {
      VariableDeclaration(path) {
        if(path.node.kind === 'const') { // 如果kind为const,则转换为var
            path.node.kind = 'var'
        }
      },
      ArrowFunctionExpression(path) {
          console.log(path)
      }
    }
    
  2. 转换AST

    要将箭头函数转换为function函数,显然是需要使用babel-types定义function函数构造方法来构造一个function函数节点,然后再将原来节点替换掉即可,所以我们得首先查一下function函数构造方法的api是啥样子的:

    从图中我们知道了function函数构造方法api,那么我们就可以直接来进行转换了:

    visitor: {
      VariableDeclaration(path) {
        if(path.node.kind === 'const') { // 如果kind为const,则转换为var
            path.node.kind = 'var'
        }
      },
      ArrowFunctionExpression(path) {
          path.replaceWith(types.functionExpression(path.node.id, path.node.params, path.node.body));
      }
    }
    

    看下效果

上面两个功能都实现了,接下来我们来实现下如何将箭头函数里的形参及函数体内的变量d改成变量m。还是按照上面两个步骤来:

  1. 添加visitor对象的节点监听方法

    还是从AST中找到对应的箭头函数类型的监听方法的写法

    即:Identifier

    所以监听方式如下:

    visitor: {
      VariableDeclaration(path) {
        if(path.node.kind === 'const') { // 如果kind为const,则转换为var
            path.node.kind = 'var'
        }
      },
      ArrowFunctionExpression(path) {
          path.replaceWith(types.functionExpression(path.node.id, path.node.params, path.node.body));
      },
      Identifier(path) {
          console.log(path)
      }
    }
    
  2. 转换AST

    从AST中我们知道Identifier类型的节点有一个name属性来标识该节点的名称,所以我们要改变此节点名称,就是要对name属性进行重新赋值,即:

    visitor: {
      VariableDeclaration(path) {
        if(path.node.kind === 'const') { // 如果kind为const,则转换为var
            path.node.kind = 'var'
        }
      },
      ArrowFunctionExpression(path) {
          path.replaceWith(types.functionExpression(path.node.id, path.node.params, path.node.body));
      },
      Identifier(path) {
          if (path.node.name === 'd') { // 如果变量名是d,则调整为m
            path.node.name = 'm';
          }
      }
    }
    

    看下效果:

    如图所示,变量d也被成功转换m,但是这里有个问题,假设我在源码中箭头函数外也声明了一个d变量,那也这个d变量也会改成m,例如源码为:

    const a = 1;
    const b = 10;
    const c = (d) => {
      var e = 20;
      console.log(d);
    }
    const d = 30;
    

    执行下上述代码:

    可以看到我们的插件也把其他地方的d变量改成m了,所以这里需要调整下转换代码,因为我们只需要转换箭头函数内的d变量,因此我们应该在箭头函数的监听方式对当前作用域内进行转换即可:

    visitor: {
      VariableDeclaration(path) {
        if(path.node.kind === 'const') { // 如果kind为const,则转换为var
            path.node.kind = 'var'
        }
      },
      ArrowFunctionExpression(path) {
          // 对当前节点进行traverse
          path.traverse({
            Identifier(path) {
                if (path.node.name === 'd') {
                   path.node.name = 'm';
                }
            }
          });
          path.replaceWith(types.functionExpression(path.node.id, path.node.params, path.node.body));
      }
    }
    

    执行下代码

    这下,箭头函数外的变量d就不会被转换了。

总结

到这里,我们就完成一个babel插件了,源码请点击这里获取:github.com/canfoo/babe…

可能有些读者会认为本文实现的插件功能比较简单,也确实比较简单,但是本文的目的主要是表述babel插件是开发方式,所以希望小小经验对你们有帮助的😊。