likes
comments
collection
share

简单易懂的babel插件开发

作者站长头像
站长
· 阅读数 47

babel

babel相信大家应该都不陌生,作为JS编译器,Babel接收输入的JS代码,经过内部处理流程,最终输出修改后的JS代码。

Babel内部,会执行如下步骤:

  • Input Code解析为AST(抽象语法树),这一步称为parsing
  • 编辑AST,这一步称为transforming
  • 将编辑后的AST输出为Output Code,这一步称为printing

我们编写的babel插件就是在第二步transforming来进行处理,最后编译输出

实现babel插件

一个好用的线上js转化ast的工具 => 在线js代码转换为AST

我们来看看babel插件是如何开发的,首先初始化一下,然后安装如下所需依赖

yarn init
yarn add @babel/cli @babel/core @babel/types -D


对应文件描述
test/compiled.js babel  编译后的代码的输出文件
test/index.js  初始原代码
test/plugin.js  babel插件逻辑处理
.babelrc  插件配置文件

文件结构如下

简单易懂的babel插件开发

我们需要在 package.json 增加命令,该命令就是通过babel编译 index.js 文件并输出到文件compiled.js

"scripts": {
    "example": "babel ./test/index.js --out-file ./test/compiled.js "
}

然后我们来看看典型的babel插件结构 plugins.js文件

module.exports = function(e) {
  return {
    visitor: {
      Identifier (path) {}
    }
  }
}

可以看到babel插件导出一个函数,函数里面return一个对象,其中的visitor属性也是最核心

babel 在使用 @babel/traverse 对 AST 进行深度遍历时,会 访问 每个 AST 节点,这个便是跟我们的 visitor 有关。babel 会在 访问 AST 节点的时候,调用 visitor  中对应 节点类型 的方法,这便是 babel 插件暴露给开发者的核心。

改变值

我们直接看个栗子

const val = 1

我们来看看上面例子对应的ast样子,可以通过上面提供的在线网址进行转化

简单易懂的babel插件开发

可以看到上述代码块的节点类型是VariableDeclaration,value对应的节点类型为NumericLiteral,我们现在试着通过babel插件来改变一下val的值

// .babelrc
{
  "plugins": ["./test/plugin"]
}

// index.js
const val = 1

// plugin.js
module.exports = function () {
  return {
    visitor: {
      NumericLiteral(path) {
        path.node.value = 2
      }
    }
  }
}

在命令行中执行yarn example,等执行完毕出现类似 Done in xxs就代表编译成功,然后打开compiled.js文件,可以看到显示const val = 2; 说明我们编写的插件有效

通过这个例子应该可以更加生动知道visitor属性的作用,它会对ast进行深度遍历,匹配到对应的节点类型钩子函数进行逻辑处理。

消灭console

当我们在进行项目开发的时候,经常会通过console.log()在控制台输出一些信息,但是我们又不希望在线上环境出现这些输出信息,我们可以写个babel插件来删除对应的console

const val = 1
console.log(val)

首先我们来看看上述代码对应的ast

简单易懂的babel插件开发

只截图了console对应的ast结构,可以看到console.log 对应的节点类型是CallExpression(函数调用类型),并且其有callee属性,其name值为console

插入一个知识点,怕大家对后面一些方法不清楚

babel库中path常用方法

  • path.remove() 节点删除
  • path.get(key) 获取当前路径下的子孙节点
  • path.replaceWith(newNode) (单)节点替换函数
// index.js
const val = 1
console.log(val)

// plugin.js
module.exports = function () {
  return {
    visitor: {
      CallExpression(path) {
        // 获取callee节点
        const callee = path.get('callee')
        // // 获取callee节点下的object节点
        const { node } = callee.get('object')
        
        if (callee && node.name === 'console') {
          path.remove()
        }
      }
    }
  }
}

执行yarn example,执行完毕打开compiled.js文件,可以看到只有const val = 1;,说明我们编写的插件又成功了😊

可选链操作符

es6新增的 可选链操作符?. )相信大家都听过或者用过,如果要在项目中使用它,需要安装babel插件(@babel/plugin-proposal-optional-chaining)并在.babelrc文件中配置如下即可使用

{
  "plugins": [
     "@babel/plugin-proposal-optional-chaining"
  ]
}

接下来,我们也来试着开发一个可选链操作符插件

首先我们得先知道可选链操作符对应转化的代码是啥样

// 当a等于null/undefined时,则直接返回undefined否则返回a.b
const val = a?.b 
      ⇩
      ⇩
const val = a === null || a === void 0 ? void 0 : a.b

知道长啥样,我们来看看可选链操作符对应的ast结构

简单易懂的babel插件开发

可以看到可选链操作符对应的节点类型为OptionalMemberExpressionobject节点对应的是变量a,property节点对应的是变量a的属性b

要怎么将可选链对应的表达式进行转换呢?先看几个创建 AST 节点的示例方便我们后面转换

创建 AST 节点写法示例

// 创建三元表达式 示例 4 > 3 ? 4 : 3
t.conditionalExpression(
  t.binaryExpression(
    '>',
    t.numericLiteral(4),
    t.numericLiteral(3)
  ),
  t.numericLiteral(4),
  t.numericLiteral(3)
)

// 创建逻辑表达式 示例 num || 0
t.logicalExpression(
  '||',
  t.identifier('num'),
  t.numericLiteral(0)
)

// 创建二元表达式 示例 1+2
t.binaryExpression(
  '+', // 操作符。 还支持:==, ===, != 等
  item1, // 左操作数
  item2
)

// 创建获取对象的属性 示例 obj.name
t.memberExpression(
  t.identifier('obj'),
  t.identifier('name')
)

// 创建null
t.nullLiteral()

上面的示例足够我们来实现可选链操作符插件

// const val = a === null || a === void 0 ? void 0 : a.b

const t = require('@babel/types')
module.exports = function () {
  const transCondition = (node) => {
    return t.conditionalExpression( // 三元表达式
      t.logicalExpression( // 逻辑表达式
        '||',
        t.binaryExpression('===', node.object, t.nullLiteral()), // 二元表达式
        t.binaryExpression(
          '===',
          node.object,
          t.unaryExpression('void', t.numericLiteral(0))
        )
      ),
      t.unaryExpression('void', t.numericLiteral(0)),
      t.memberExpression( // 获取对象的属性a.b
        node.object,
        node.property
      )
    )
  }
  return {
    visitor: {
      OptionalMemberExpression(path) {
        // 替换节点
        path.replaceWith(transCondition(path.node))
      },
    },
  }
}

为什么要用void 0 替代undefined

  1. 某些情况下用undefined判断存在风险,因undefined有被修改的可能性,但是void 0返回值一定是undefined

  2. 兼容性上void 0 基本所有的浏览器都支持

  3. void 0比undefined字符所占空间少。

执行yarn example,执行完毕打开compiled.js文件,可以看到const val = a === null || a === void 0 ? void 0 : a.b;,我们编写的插件又成功了😊😊😊