编写你的第一个 Babel 插件

73 阅读2分钟

先从一个接收了当前babel对象作为参数的 function 开始。

export default function(babel) {
  // plugin contents
}

由于你将会经常这样使用,所以直接取出 babel.types 会更方便:(译注:这是 ES2015 语法中的对象解构,即 Destructuring)

export default function({ types: t }) {
  // plugin contents
}

接着返回一个对象,其 visitor 属性是这个插件的主要访问者。

export default function({ types: t }) {
  return {
    visitor: {
      // visitor contents
    }
  };
};

Visitor 中的每个函数接收2个参数:pathstate

export default function({ types: t }) {
  return {
    visitor: {
      Identifier(path, state) {},
      ASTNodeTypeHere(path, state) {}
    }
  };
};

让我们快速编写一个可用的插件来展示一下它是如何工作的。下面是我们的源代码:

foo === bar;

其 AST 形式如下:

{
  type: "BinaryExpression",
  operator: "===",
  left: {
    type: "Identifier",
    name: "foo"
  },
  right: {
    type: "Identifier",
    name: "bar"
  }
}

我们从添加 BinaryExpression 访问者方法开始:

export default function({ types: t }) {
  return {
    visitor: {
      BinaryExpression(path) {
        // ...
      }
    }
  };
}

然后我们更确切一些,只关注哪些使用了 ===BinaryExpression

visitor: {
  BinaryExpression(path) {
    if (path.node.operator !== "===") {
      return;
    }

    // ...
  }
}

现在我们用新的标识符来替换 left 属性:

BinaryExpression(path) {
  if (path.node.operator !== "===") {
    return;
  }

  path.node.left = t.identifier("sebmck");
  // ...
}

于是如果我们运行这个插件我们会得到:

sebmck === bar;

现在只需要替换 right 属性了。

BinaryExpression(path) {
  if (path.node.operator !== "===") {
    return;
  }

  path.node.left = t.identifier("sebmck");
  path.node.right = t.identifier("dork");
}

这就是我们的最终结果了:

sebmck === dork;

现在我们来手写一个为开发环境debug的插件,功能是如果在开发环境下添加debug判断就继续执行条件,到了正式环境自动把debug条件下的逻辑去掉,不用自己手动去删除,达到解放你的双手。

在项目目录下创建index.js,代码如下:

module.exports = function ({ types: t }) {
  return {
    visitor: {
      Identifier(path) {
        const parentNodeIsIfStatement = t.isIfStatement(path.parent);
        const isDebug = path.node.name === "DEBUG";
        if (isDebug && parentNodeIsIfStatement) {
          const stringNode = t.stringLiteral("DEBUG");
          path.replaceWith(stringNode);
        }
      },
      StringLiteral(path) {
        console.log(path.node.value);
        const parentNodeIsIfStatement = t.isIfStatement(path.parent);
        const isDebug = path.node.value === "DEBUG";
        if (isDebug && parentNodeIsIfStatement) {
          if (process.env.NODE_ENV === "production") {
            path.parentPath.remove();
          }
        }
      },
    },
  };
};


然后在项目中创建babel.config.js,我这个是在vue项目中测试的,

截屏2023-03-18 09.40.21.png

代码如下:

module.exports = {
  presets: ["@vue/cli-plugin-babel/preset"],
  plugins: ["../index.js"],
};

App.js代码如下:

<template>
  <div>
    example
    <button @click="handleClick">click</button>
  </div>
</template>

<script>
export default {
  name: "App",
  setup() {
    const handleClick = () => {
      console.log("click"); 
      if (DEBUG) {
        console.log("for debug")
        const a = 10;
        const b = 20;
        console.log(a + b);
      }
    };

    return {
      handleClick,
    };
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

截屏2023-03-18 09.51.25.png

可以看到开发环境下是进入debug判断条件里面的,执行打包之后:

截屏2023-03-18 10.01.52.png

完工,是不是很简单😁