从实现一个babel插件来理解babel

前言

自动在 try-catch 块中添加 错误捕获方法

先了解一下 babel 的基础知识

官方文档才是最好的教程

获取代码的 AST

babel 是什么?

这里引用官方的话说,Babel 是一个 JavaScript 编译器,特别是一个源代码到源代码的编译器,通常被称为“转译器”。这意味着你给 Babel 一些 JavaScript 代码,Babel 修改代码,并生成新的代码。

babel 的工作流程

  1. 解析

    • 使用@babel/parser解析器进行词法和语法解析,生成 AST
  2. 转换(babel-plugin 工作在这一层)

    • 使用@babel/traverse对 AST 进行深度遍历,并且提供钩子来协助我们在遍历到某种 AST 节点的时候进行处理,比如callExpression
  3. 生成

    • 使用@babel/generator将处理后的 AST 转换成最终可以在浏览器上运行的代码和源码映射(sourcemap)

总结成以下图片: babel的工作流程

开发 babel 插件你需要知道哪些库

  • @babel/types:一个用于 AST 节点的 Lodash 工具库,它包含构造、验证以及变换 AST 节点的方法。这个库提供了 babel AST 所有节点类型,用户可以使用这个库构造出一个新的 AST 节点,或者判断某个节点是什么类型等。
  • @babel/template:它不像@babel/types 通过生成 AST 节点的方式来处理 AST 节点,它能让你编写字符串形式且带有占位符的代码来替代手动编码,尤其是生成大规模 AST 的时候。
  • @babel/core:babel 工作流程中三个插件的合体。

babel 插件开发基础

如何访问到需要处理的 AST 节点

babel 在使用@babel/traverse对 AST 进行深度优先遍历时,会访问每个 AST 节点,这个便跟我们的 visitor(访问器模式) 有关了,这就是 babel 暴露给开发者的核心。

看图 1,catch 块的 AST 节点名为 CatchClause,所以我们可以根据以下方式访问到 catch 块:

visitor: {
  CatchClause(path,state){}
}

具体有哪些节点可看这个链接:babel-ast-node。不过一般我们都是通过 AST Explorer 来直接转化成我们想知道的节点名。

如何获取到 AST 节点

当我们如上文访问一个函数调用表达式时,babel 会向我们的方法传递一些参数:CatchClause(path,state){},其中path对象便是我们获取 AST 节点的入口,其中path对象比较重要的属性有以下:

  • node 当前 AST 节点信息
  • parent 父节点的 AST 节点信息
  • parentNode 父节点的 path 信息,通过这个属性可以一路向上找
  • scope 作用域
  • context 当前 traverse 的上下文

通过 path.node ,我们便拿到了一个 AST 节点。

如何操作 AST 节点

由于 JavaScript 对于对象是地址引用,因此我们想要操作 AST 只要操作这个 node 对象即可,更详细的说是通过path对象来操作 AST 节点,不需要额外有其他返回操作,babel 会使用原来的引用。

但是直接对 node 对象(也就是 AST 节点)进行操作成本不低,特别是需要构造出一些比较复杂的 AST 节点对原节点 进行 插入(增)替换(改)等操作的时候。所以 @babel/types 也贴心的提供了诸多构造出一个新的 AST 节点的方法,比如:

  • 构造一个 解构对象 : t.spreadElement(t.identifier(data))
  • 构造一个 对象表达式: t.objectExpression([t.spreadElement(t.identifier(data))])
  • 在节点内新增一行注释: t.addComment(node, 'inner', 'commment')
  • ...

这样我们便实现了对 AST 节点的增删改查。如果你的 AST 极其复杂,可以考虑使用上文提到的 @babel/template 来将一段字符串转化为 AST 节点,从而不必再从一个一个最原始的节点开始构造。

开发我们的 babel 插件吧

假设我们的.babelrc 为:

{
   "babel-catch-plugin",
   {
      "logger":"console",
      "method":"log"
   }
}

那我们可以编写插件如下:

// babel-core会向函数注入babel-types和babel-template属性
const babelCatchPlugin = ({ types: t, template }) => {
  return {
    name: "babel-catch-plugin",
    visitor: {
      CatchClause(path, state) {
        // babel配置的options
        const options = state.opts
        // 获取catch块函数的参数,假设我们这里把错误命名为 e
        const error = path.node.param
        // 生成logger的ast节点,console.log(e)
        const ast = template.ast(`${options.logger}.${method}(${error})`)
        // 插入ast树
        path.get("body").unshiftContainer("body", ast)
      },
    },
  }
}

export default babelCatchPlugin

从问题完善插件

  1. catch 块中如果已经有错误捕获了,就不添加了。
// 我们可以通过遍历当前节点下的ast,如果存在错误捕获就提前return
let isFlag = false

outerPath.traverse({
  MemberExpression(innerPath) {
    if (t.isIdentifier(innerPath.node.property, { name: options.logger })) {
      isFlag = true
      // innerPath.stop()结束遍历,innerPath.skip()结束当前path遍历
      innerPath.stop()
    }
  },
})

if (isFlag) {
  return
}
  1. 为什么 catch(E)和 catch(_)会报 babel-template 错误?

因为你直接用的是 template而不是通过 template.ast 生成 ast 节点。template 中可接收拥有占位符的字符串生 AST 节点,下划线_ , % 和大写字符都是占位符的关键词。而template.ast不管占位符,直接将传入的字符串转化为 AST 节点。

  1. visitor 函数的参数 state 和 this 对象有啥区别?

两者都是保存的是访问当前节点文件的信息,没有区别,推荐使用 state。

测试插件

我喜欢通过主动编译的形式测试插件,可以转化自己所有想测试的代码。

"scripts": {
   "test": "babel ./test/index.js -o ./test/test.js --config-file ./test/.babelrc "
},
  • 创建一个 test 文件夹存想测试的代码在 index.js 中
  • 配置引用了当前插件编译后过文件夹的 .babelrc 文件
  • npm run test
try {
} catch (e) {}

转化成:

try {
} catch (e) {
  console.log(e)
}

以上方式简单粗暴,但是会有以下缺点:

  1. 当测试用例过多时,./test/index.js文件就会变得很庞大
  2. 每个测试用例没有具体的说明测试的什么,混乱不堪
  3. 当有测试用例不正确时,只能通过肉眼的角度来查找,效率极低

基于以上原因,强烈建议通过babel-plugin-tester来测试 babel 插件

配置 babel-plugin-tester

  1. yarn add babel-plugin-tester jest
  2. 新建 src 同级目录 test,在 test 目录下新建fixtures目录存放测试用例和index.test.js 文件配置babel-plugin-tester
  3. index.test.js 配置实例如下
import pluginTester from "babel-plugin-tester"
import path from "path"
import babelCatchPlugin from "../src"

pluginTester({
  plugin: babelCatchPlugin,
  pluginName: "babel-catch-plugin",
  title: "test plugin",
  pluginOptions: {
    logger: "console",
    method: "log",
  },
  // 使用 jest 的 snapshot
  snapshot: true,
  // 测试用例读取的目录
  fixtures: path.join(__dirname, "__fixtures__"),
})
  1. 在 fixtures 中可新建各种用例文件夹,code.js则是对插件的输入代码,output.js则是期望的输出代码,options.json可对插件的 option 进行重新配置,更多 API 查看官网 test
  2. 在 package.json 的 scripts 字段添加"test": "jest --verbose",,每次需要测试在控制台输入yarn test即可

参考

Published under  on .

Last updated on .

pipihua

我是皮皮花,一个前后端通吃的前端攻城狮,如果感觉不错欢迎点击小心心♥(ˆ◡ˆԅ) star on GitHub!