如何写⼀个babel插件?

100 阅读3分钟

Babel是JavaScript编译器,可以将未来的JavaScript语法转译为当前JavaScript语法。在Babel中,一个插件就是一个函数,这个函数接收一个babel对象,包含了所有Babel转译JavaScript的函数,如types类型、path路径等。一个Babel插件可以通过改变抽象语法树(AST),修改或删除节点的方式,针对源码进行转换。

Babel解析成AST,然后插件更改AST,最后由Babel输出代码

那么Babel的插件模块需要你暴露⼀个function,function内返回visitor

module.export = function(babel){
    return {
        visitor:{
        }
    }
}

visitor是对各类型的AST节点做处理的地⽅,那么我们怎么知道Babel⽣成了的AST有哪些节点呢?

很简单,你可以把Babel转换的结果打印出来,或者这⾥有传送⻔: AST explorer

这⾥我们看到 const result = 1 + 2 中的 1 + 1 是⼀个 BinaryExpression 节点,那么在visitor中,我们就处理这个节点

var babel = require('babel-core');

var t = require('babel-types');

const visitor = {

    BinaryExpression(path) {

        const node = path.node;

        let result;

    // 判断表达式两边,是否都是数字

    if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {

        // 根据不同的操作符作运算

        switch (node.operator) {

            case "+":

                result = node.left.value + node.right.value;

                break

            case "-":

                result = node.left.value - node.right.value;

                break;

            case "*":

                result = node.left.value * node.right.value;

                break;

            case "/":

                result = node.left.value / node.right.value;

                break;

            case "**":

                let i = node.right.value;

                while (--i) {

                    result = result || node.left.value;

                    result = result * node.left.value;

                }

            break;

            default:

            }

        }

        // 如果上⾯的运算有结果的话

        if (result !== undefined) {

            // 把表达式节点替换成number字⾯量

            path.replaceWith(t.numericLiteral(result));

        }

      }

    };

    module.exports = function (babel) {

        return {

            visitor

        };

     }

插件写好了,我们运⾏下插件试试


const babel = require("babel-core");

const result = babel.transform("const result = 1 + 2;",{

plugins:[

    require("./index")

]

});

console.log(result.code); // const result = 3;

与预期⼀致,那么转换 const result = 1 + 2 + 3 + 4 + 5; 呢?

结果是: const result = 3 + 3 + 4 + 5;

这就奇怪了,为什么只计算了 1 + 2 之后,就没有继续往下运算了?

我们看⼀下这个表达式的AST树

你会发现Babel解析成表达式⾥⾯再嵌套表达式。

表达式( 表达式( 表达式( 表达式(1 + 2) + 3) + 4) + 5)

⽽我们的判断条件并不符合所有的,只符合 1 + 2

// 判断表达式两边,是否都是数字
if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {}

那么我们得改⼀改

第⼀次计算 1 + 2 之后,我们会得到这样的表达式

表达式( 表达式( 表达式(3 + 3) + 4) + 5)

其中 3 + 3 ⼜符合了我们的条件, 我们通过向上递归的⽅式遍历⽗级节点

⼜转换成这样:

表达式( 表达式(6 + 4) + 5)

表达式(10 + 5)

15
// 如果上⾯的运算有结果的话

if (result !== undefined) {

// 把表达式节点替换成number字⾯量

path.replaceWith(t.numericLiteral(result));

let parentPath = path.parentPath;

// 向上遍历⽗级节点

parentPath && visitor.BinaryExpression.call(this, parentPath);

}

到这⾥,我们就得出了结果 const result = 15;

那么其他运算呢:


const result = 100 + 10 - 50 >>> const result = 60;

const result = (100 / 2) + 50 >>> const result = 100;

const result = (((100 / 2) + 50 * 2) / 50) ** 2 >>> const result = 9;

下面是一个babel插件的实现示例,我们需要将源代码中所有箭头函数表达式转换为普通的函数表达式:

// 将所有箭头函数转换为普通函数
const arrowToNormal = babel => {
  const { types: t } = babel;
  return {
    visitor: {
      ArrowFunctionExpression(path) {
        // 获取函数参数
        const params = path.node.params;
        // 获取函数体
        const body = path.node.body;
        // 创建函数节点
        const func = t.functionExpression(null, params, body);
        // 将原有节点替换为新建节点
        path.replaceWith(func);
      },
    },
  };
};

module.exports = arrowToNormal;

在这个示例中,插件的名称为 arrowToNormal,它接收 babel 对象并返回一个对象。对象 visitor 是一个包含了多个键值对的对象,其键名是针对不同类型节点类型的方法。因为我们需要转换所有箭头函数,所以这里使用 ArrowFunctionExpression 方法。

ArrowFunctionExpression 方法会在抽象语法树(AST)中每次找到箭头函数的节点时被调用,接着提取箭头函数的参数和函数体,再将其创建为函数表达式节点。

最后,我们需要在项目的Babel配置文件中添加插件配置:

module.exports = {
  plugins: [
    //使用自定义的plugin
    "babel-plugin-arrow-to-normal",
  ],
};

这是一个简单的Babel插件示例,表达了一些基本的 Babel 插件结构,可以根据需要进一步细化实现更复杂的转换功能。