Babel_如何写一个Babel插件

更新日期: 2019-03-18阅读: 2.9k标签: babel

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函数的处理可以参考上面的源码.

来自:https://segmentfault.com/a/1190000018562241


链接: https://fly63.com/article/detial/2424

Babel中的stage-0,stage-1,stage-2以及stage-3的作用(转)

大家知道,将ES6代码编译为ES5时,我们常用到Babel这个编译工具。大家参考一些网上的文章或者官方文档,里面常会建议大家在.babelrc中输入如下代码

深入理解Babel原理及其使用,babel把ES6转成ES5的原理是什么?

文的babel使用场景局限于babel配合webpack来转译输出es5的js代码,babel的命令行、以代码形式调用或node环境下这些统统都不会涉及。Babel使用的难点主要在于理解polyfill、runtime和core-js。

ES6之Babel的各种坑总结【转载】

自从 Babel 由版本5升级到版本6后,在安装和使用方式上与之前大相径庭,于是写了这篇入坑须知,以免被新版本所坑。坑一本地安装和全局安装 、坑二编译插件、坑三babel-polyfill插件

babel-preset-env:一个帮你配置babel的preset

babel-preset-env 一个帮你配置babel的preset,根据配置的目标环境自动采用需要的babel插件。babel-preset-env 功能类似 babel-preset-latest,优点是它会根据目标环境选择不支持的新特性来转译

babel-polyfill使用与性能优化

本文主要内容包括:什么是babel-polyfill,如何使用,如何通过按需加载进行性能优化。babel只负责语法转换,比如将ES6的语法转换成ES5。但如果有些对象、方法,浏览器本身不支持,此时需要引入babel-polyfill来模拟实现这些对象、方法。

babel的初步了解

由于现在前端出现了很多非es5的语法,如jsx,.vue,ts等等的格式和写法。babel其实是一个解释器,它主要讲进行中的代码分为三个阶段执行:解释,转换,生成。

深入类和继承内部原理+Babel和 TypeScript 之间转换

在 JavaScript 中,没有基本类型,创建的所有东西都是对象。例如,创建一个新字符串,与其他语言不同,在 JavaScript 中,字符串或数字的声明会自动创建一个封装值的对象,并提供不同的方法,甚至可以在基本类型上执行这些方法。

使用 Webpack 与 Babel 配置 ES6 开发环境

在项目根目录下新建一个配置文件—— webpack.config.js 文件:执行编译打包命令,完成后打开 bundle.js 文件发现 isNull 和 unique 两个函数没有被编译,和 webpack 官方说法一致:webpack 默认支持 ES6 模块语法,要编译 ES6 代码依然需要 babel 编译器。

初学 Babel 工作原理

Babel 对于前端开发者来说应该是很熟悉了,日常开发中基本上是离不开它的。我们已经能够熟练地使用 es2015+ 的语法。但是对于浏览器来说,可能和它们还不够熟悉,我们得让浏览器理解它们,这就需要 Babel

Babel是什么?

Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!