自己写一个Babel插件

栏目: JavaScript · 发布时间: 7年前

内容简介:之前看到一位大佬的博客, 介绍了babel的原理, 以及如何写一个babel的插件, 抱着试试看的想法, 照葫芦画瓢的自己写了一个简单的babel插件, 该插件的作用就是将代码字符串中的表达式, 直接转换为对应的计算结果。例如: const code =建议先阅读一下这一篇Babel对代码进行转换,会将JS代码转换为AST抽象语法树(解析),对树进行静态分析(转换),然后再将语法树转换为JS代码(生成)。每一层树被称为节点。每一层节点都会有type属性,用来描述节点的类型。其他属性用来进一步描述节点的类型
自己写一个Babel插件

前言

之前看到一位大佬的博客, 介绍了babel的原理, 以及如何写一个babel的插件, 抱着试试看的想法, 照葫芦画瓢的自己写了一个简单的babel插件, 该插件的作用就是将代码字符串中的表达式, 直接转换为对应的计算结果。例如: const code = const result = 1 + 1 转化为const code = const result = 2 。当然这一篇文章非常的浅显, 但是对了解Babel的原理以及AST的基本概念是足够的了。

相关链接

插件的源码

const t = require('babel-types')

const visitor = {
  // 二元表达式类型节点的访问者
  BinaryExpression(path) { 
    // 子节点
    // 访问者会一层层遍历AST抽象语法树, 会树形遍历AST的BinaryExpression类型的节点
    const childNode = path.node
    let result = null
    if (
      t.isNumericLiteral(childNode.left) &&
      t.isNumericLiteral(childNode.right)
    ) {
      const operator = childNode.operator
      switch (operator) {
        case '+':
          result = childNode.left.value + childNode.right.value
          break
        case '-':
          result = childNode.left.value - childNode.right.value
          break
        case '/':
          result = childNode.left.value / childNode.right.value
          break
        case '*':
          result = childNode.left.value * childNode.right.value
          break
      }
    }
    if (result !== null) {
      // 替换本节点为数字类型
      path.replaceWith(
        t.numericLiteral(result)
      )
      if (path.parentPath) {
        const parentType = path.parentPath.type
        if (visitor[parentType]) {
          visitor[parentType](path.parentPath)
        }
      }
    }
  },
  // 属性表达式
  MemberExpression(path) {
    const childNode = path.node
    let result = null
    if (
      t.isIdentifier(childNode.object) &&
      t.isIdentifier(childNode.property) &&
      childNode.object.name === 'Math'
    ) {
      result = Math[childNode.property.name]
    }
    if (result !== null) {
      const parentType = path.parentPath.type
      if (parentType !== 'CallExpression') {
        // 替换本节点为数字类型
        path.replaceWith(
          t.numericLiteral(result)
        )
        if (visitor[parentType]) {
          visitor[parentType](path.parentPath)
        }
      }
    }
  },
  // 一元表达式
  UnaryExpression (path) {
    const childNode = path.node
    let result = null
    if (
      t.isLiteral(childNode.argument)
    ) {
      const operator = childNode.operator
      switch (operator) {
        case '+':
          result = childNode.argument.value
          break
        case '-':
          result = -childNode.argument.value
          break
      }
    }
    if (result !== null) {
      // 替换本节点为数字类型
      path.replaceWith(
        t.numericLiteral(result)
      )
      if (path.parentPath) {
        const parentType = path.parentPath.type
        if (visitor[parentType]) {
          visitor[parentType](path.parentPath)
        }
      }
    }
  },
  // 函数执行表达式
  CallExpression(path) {
    const childNode = path.node
    // 结果
    let result = null
    // 参数的集合
    let args = []
    // 获取函数的参数的集合
    args = childNode.arguments.map(arg => {
      if (t.isUnaryExpression(arg)) {
        return arg.argument.value
      }
    })
    if (
      t.isMemberExpression(childNode.callee)
    ) {
      if (
        t.isIdentifier(childNode.callee.object) &&
        t.isIdentifier(childNode.callee.property) &&
        childNode.callee.object.name === 'Math'
      ) {
        result = Math[childNode.callee.property.name].apply(null, args)
      }
    }
    if (result !== null) {
      // 替换本节点为数字类型
      path.replaceWith(
        t.numericLiteral(result)
      )
      if (path.parentPath) {
        const parentType = path.parentPath.type
        if (visitor[parentType]) {
          visitor[parentType](path.parentPath)
        }
      }
    }
  }
}

module.exports = function () {
  return {
    visitor
  }
}
复制代码

基本概念

建议先阅读一下这一篇 文档

babel工作的原理

Babel对代码进行转换,会将JS代码转换为AST抽象语法树(解析),对树进行静态分析(转换),然后再将语法树转换为JS代码(生成)。每一层树被称为节点。每一层节点都会有type属性,用来描述节点的类型。其他属性用来进一步描述节点的类型。

// 将代码生成对应的抽象语法树

// 代码
const result = 1 + 1

// 代码生成的AST
{
  "type": "Program",
  "start": 0,
  "end": 20,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 20,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 20,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 12,
            "name": "result"
          },
          "init": {
            "type": "BinaryExpression",
            "start": 15,
            "end": 20,
            "left": {
              "type": "Literal",
              "start": 15,
              "end": 16,
              "value": 1,
              "raw": "1"
            },
            "operator": "+",
            "right": {
              "type": "Literal",
              "start": 19,
              "end": 20,
              "value": 1,
              "raw": "1"
            }
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}
复制代码

解析

解析分为词法解析和语法分析, 词法解析将代码字符串生成令牌流, 而语法分析则会将令牌流转换成AST抽象语法树

转换

节点的路径(path)对象上, 会暴露很多添加, 删除, 修改AST的API, 通过操作这些API实现对AST的修改

生成

生成则是通过对修改后的AST的遍历, 生成新的源码

遍历

AST是树形的结构, AST的转换的步骤就是通过访问者对AST的遍历实现的。访问者会定义处理不同的节点类型的方法。遍历树形结构的同时,, 遇到对应的节点类型会执行相对应的方法。

访问者

Visitors访问者本身就是一个对象,对象上不同的属性, 对应着不同的AST节点类型。例如,AST拥有BinaryExpression(二元表达式)类型的节点, 如果在访问者上定义BinaryExpression属性名的方法, 则这个方法在遇到BinaryExpression类型的节点, 就会执行, BinaryExpression方法的参数则是该节点的路径。 注意对每一个节点的遍历会执行两次, 进入节点一次, 退出节点一次

const visitors = {
  enter (path) {
    // 进入该节点
  },
  exit (path) {
    // 退出该节点
  }
}
复制代码

路径

每一个节点都拥有自身的路径对象(访问者的参数, 就是该节点的路径对象), 路径对象上定义了不同的属性和方法。例如: path.node代表了该节点的子节点, path.parent则代表了该节点的父节点。path.replaceWithMultiple方法则定义的是替换该节点的方法。

访问者中的路径

节点的路径信息, 存在于访问者的参数中, 访问者的默认的参数就是节点的路径对象

第一个插件

我们来写一个将 const result = 1 + 1 字符串解析为 const result = 2 的简单插件。我们首先观察这段代码的AST, 如下。

我们可以看到BinaryExpression类型(二元表达式类型)的节点, 中定义了这段表达式的主体(1 + 1), 1 分别是BinaryExpression节点的子节点left,BinaryExpression节点的子节点right,而加号则是BinaryExpression节点的operator的子节点

// 经过简化之后
{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "result"
          },
          "init": {
            "type": "BinaryExpression",
            "left": {
              "type": "Literal",
              "value": 1
            },
            "operator": "+",
            "right": {
              "type": "Literal",
              "value": 1
            }
          }
        }
      ]
    }
  ]
}
复制代码

接下来我们来处理这个类型的节点,代码如下

const t = require('babel-types')


const visitor = {
  BinaryExpression(path) { 
    // BinaryExpression节点的子节点
    const childNode = path.node
    let result = null
    if (
      // isNumericLiteral是babel-types上定义的方法, 用来判断节点的类型
      t.isNumericLiteral(childNode.left) &&
      t.isNumericLiteral(childNode.right)
    ) {
      const operator = childNode.operator
      // 根据不同的操作符, 将left.value, right.value处理为不同的结果
      switch (operator) {
        case '+':
          result = childNode.left.value + childNode.right.value
          break
        case '-':
          result = childNode.left.value - childNode.right.value
          break
        case '/':
          result = childNode.left.value / childNode.right.value
          break
        case '*':
          result = childNode.left.value * childNode.right.value
          break
      }
    }
    if (result !== null) {
      // 计算出结果后
      // 将本身的节点,替换为数字类型的节点
      path.replaceWith(
        t.numericLiteral(result)
      )
    }
  }
}
复制代码

我们定义一个访问者, 在上面定义BinaryExpression的属性的方法。运行结果如我们预期, const result = 1 + 1被处理为了const result = 2。但是我们将代码修改为const result = 1 + 2 + 3发现结果变为了 const result = 3 + 3, 这是为什么呢? 我们来看一下1 + 2 + 3的AST抽象语法树.

// 经过简化的AST

type: 'BinaryExpression'
  - left
    - left
      - left
        type: 'Literal'
        value: 1
      - opeartor: '+'
      - right
        type: 'Literal'
        value: 2
    - opeartor: '+'
    - right
      type: 'Literal'
      value: 3


复制代码

我们上面的代码的判断条件是。t.isNumericLiteral(childNode.left) && t.isNumericLiteral(childNode.right), 在这里只有最里层的AST是满足条件的。因为整个AST结构类似于, (1 + 2) + 3 => (left + rigth) + right。

解决办法是,将内部的 1 + 2的节点替换成数字节点3之后,将数字节点3的父路径(parentPath)重新执行BinaryExpression的方法(数字类型的3节点和right节点), 通过递归的方式,替换所有的节点。修改后的代码如下。

BinaryExpression(path) { 
  const childNode = path.node
  let result = null
  if (
    t.isNumericLiteral(childNode.left) &&
    t.isNumericLiteral(childNode.right)
  ) {
    const operator = childNode.operator
    switch (operator) {
      case '+':
        result = childNode.left.value + childNode.right.value
        break
      case '-':
        result = childNode.left.value - childNode.right.value
        break
      case '/':
        result = childNode.left.value / childNode.right.value
        break
      case '*':
        result = childNode.left.value * childNode.right.value
        break
    }
  }
  if (result !== null) {
    // 替换本节点为数字类型
    path.replaceWith(
      t.numericLiteral(result)
    )
    BinaryExpression(path.parentPath)
  }
}
复制代码

结果如我们预期, const result = 1 + 2 + 3 可以被正常的解析。但是这个插件还不具备对Math.abs(), Math.PI, 有符号的数字的处理,我们还需要在访问者上定义更多的属性。最后, 对于Math.abs函数的处理可以参考上面的源码.


以上所述就是小编给大家介绍的《自己写一个Babel插件》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Inside Larry's and Sergey's Brain

Inside Larry's and Sergey's Brain

Richard Brandt / Portfolio / 17 Sep 2009 / USD 24.95

You’ve used their products. You’ve heard about their skyrocketing wealth and “don’t be evil” business motto. But how much do you really know about Google’s founders, Larry Page and Sergey Brin? Inside......一起来看看 《Inside Larry's and Sergey's Brain》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

html转js在线工具
html转js在线工具

html转js在线工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具