本身寫一個Babel插件

前言

以前看到一位大佬的博客, 介紹了babel的原理, 以及如何寫一個babel的插件, 抱着試試看的想法, 照葫蘆畫瓢的本身寫了一個簡單的babel插件, 該插件的做用就是將代碼字符串中的表達式, 直接轉換爲對應的計算結果。例如: const code = const result = 1 + 1轉化爲const code = const result = 2。固然這一篇文章很是的淺顯, 可是對了解Babel的原理以及AST的基本概念是足夠的了。node

相關連接

插件的源碼

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
  }
}
複製代碼

基本概念

建議先閱讀一下這一篇文檔git

babel工做的原理

Babel對代碼進行轉換,會將JS代碼轉換爲AST抽象語法樹(解析),對樹進行靜態分析(轉換),而後再將語法樹轉換爲JS代碼(生成)。每一層樹被稱爲節點。每一層節點都會有type屬性,用來描述節點的類型。其餘屬性用來進一步描述節點的類型。github

// 將代碼生成對應的抽象語法樹

// 代碼
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抽象語法樹babel

轉換

節點的路徑(path)對象上, 會暴露不少添加, 刪除, 修改AST的API, 經過操做這些API實現對AST的修改app

生成

生成則是經過對修改後的AST的遍歷, 生成新的源碼函數

遍歷

AST是樹形的結構, AST的轉換的步驟就是經過訪問者對AST的遍歷實現的。訪問者會定義處理不一樣的節點類型的方法。遍歷樹形結構的同時,, 遇到對應的節點類型會執行相對應的方法。post

訪問者

Visitors訪問者自己就是一個對象,對象上不一樣的屬性, 對應着不一樣的AST節點類型。例如,AST擁有BinaryExpression(二元表達式)類型的節點, 若是在訪問者上定義BinaryExpression屬性名的方法, 則這個方法在遇到BinaryExpression類型的節點, 就會執行, BinaryExpression方法的參數則是該節點的路徑。注意對每個節點的遍歷會執行兩次, 進入節點一次, 退出節點一次ui

const visitors = {
  enter (path) {
    // 進入該節點
  },
  exit (path) {
    // 退出該節點
  }
}
複製代碼

路徑

每個節點都擁有自身的路徑對象(訪問者的參數, 就是該節點的路徑對象), 路徑對象上定義了不一樣的屬性和方法。例如: path.node表明了該節點的子節點, path.parent則表明了該節點的父節點。path.replaceWithMultiple方法則定義的是替換該節點的方法。spa

訪問者中的路徑

節點的路徑信息, 存在於訪問者的參數中, 訪問者的默認的參數就是節點的路徑對象.net

第一個插件

咱們來寫一個將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函數的處理能夠參考上面的源碼.

相關文章
相關標籤/搜索