DIY 一個 Babel 插件

Babel,是一個 JavaScript 的編譯工具,它能夠將 es6+語法的代碼,轉換爲瀏覽器兼容的低版本的代碼。它簡直就是一個神兵利器,前端工程師擁有了它,就能夠在項目中使用一些較新的 es 語法。筆者決定弄懂它,並實現一個本身的 Babel 插件。javascript

Babel 的工做原理,能夠用以下公式表述。它實際上就是接受輸入的源代碼,而後對它作一些處理和轉換,最後輸出爲目標版本的代碼。前端

const babel = sourceCode => distCode
複製代碼

在將輸入的源碼作處理或者轉換時,這就須要用到了它的插件系統。一個插件只負責處理一件事,好比@babel/plugin-transform-arrow-functions ,就是負責將箭頭函數轉換爲普通函數的插件。Babel 提供了很是多的插件,這樣就足以保證能夠將新的 es 語法轉換爲舊版本的代碼形式。想了解詳細的 Babel 插件系統,能夠查看Babel#Pluginsjava

若是不配置任何的插件,Babel 將不會對源碼作任何的處理,它只會照原樣輸出。下面舉個例子,node

const babel = require("@babel/core")

const code = ` const a = () => { console.log(1); } `

// 沒有配置任何plugin,那麼轉換以後的code將沒有任何變化
babel.transform(code, undefined, (err, result) => {
  if (err) {
    throw err
  }
  console.log(result.code)
})
複製代碼

咱們將箭頭函數使用 Babel 來轉換,可是沒有配置任何的插件,最後轉換以後的結果將和輸入的代碼一摸同樣。git

➜  babel node scripts/index.ts
const a = () => {
  console.log(1);
};
複製代碼

若是配置了@babel/plugin-transform-arrow-functions,Babel 就能正常將咱們的箭頭函數轉換爲普通函數的形式了。以下,es6

// 配置@babel/plugin-transform-arrow-functions
babel.transform(
  code,
  { plugins: ["@babel/plugin-transform-arrow-functions"] },
  (err, result) => {
    if (err) {
      throw err
    }
    console.log(result.code)
  }
)
複製代碼

轉換以後的代碼以下,github

➜  babel node scripts/index.ts
const a = function () {
  console.log(1);
};
複製代碼

對於其餘的語法形式的轉換,能夠添加其餘的插件。若是僅僅這樣,對於一個實際項目代碼的轉換,將要配置很是多的插件。爲了簡化這種形式,Babel 又提供了 Presets,簡單的說,就是將不少個插件集合從新命名爲一個新名稱。這樣,只須要配置了這個 Presets,那麼就相對於配置它所包含的全部的插件。Babel 定義了經常使用的 Presets,詳細能夠查看Babel#Presetsexpress

經過加入插件處理的方式,Babel 將會有很是好的可擴展性和可插拔性,好比 esNext 中又添加了一個新的語法糖,那麼 Babel 只須要單獨提供這個新語法處理的插件,並將它配置進去就能夠了。對於前端工程師們,也能夠根據實際業務需求,寫本身的插件,將輸入的源碼處理成本身想要的輸出。api

爲了能寫出本身的 Babel 插件,咱們就須要知道 Babel 將輸入的代碼轉換成什麼樣子,插件接受的參數又是什麼樣子,最後須要返回的值是什麼樣子。數組

AST

Babel 會將輸入的源代碼先轉換成 AST(Abstract Syntax Tree),而後將 AST 做爲參數傳給插件,插件將在 AST 上作處理,能夠添加,刪除或者改變節點。例如,咱們上面例子中箭頭函數 a,生成的 AST 大體結構以下,

"program": {
    "type": "Program",
    "body": [
      {
        "type": "VariableDeclaration",
        "declarations": [
          {
            "type": "VariableDeclarator",
            "id": {
              "type": "Identifier",
              "name": "a"
            },
            "init": {
              "type": "ArrowFunctionExpression",
              "params": [],
              "body": {
                "type": "BlockStatement",
                "body": [
                  { ↔ }
                ],
              }
            }
          }
        ],
        "kind": "const"
      }
    ],
  },
複製代碼

AST 能夠當作一棵樹,它包含了不少的節點,每一個節點都會包含一個 type 字段,這個 type 字段就是用來代表當前節點的類型,好比上面的Identifier代表是標識符,ArrowFunctionExpression代表是箭頭函數表達式。想詳細瞭解 AST 結構,能夠查看astexplorer

要處理 AST 樹,就得遍歷這顆樹,找到咱們要處理的節點位置。對於一棵樹的遍歷,有 DFS(深度優先搜索)和 BFS(廣度優先搜索)兩種方式。對於 AST 的遍歷,使用的是 DFS 方式。Babel 提供了@babel/traverse來遍歷它,能夠很方便的找到須要處理的節點位置。例如上面的例子,咱們能夠像下面這樣找到console.log(1)1這個節點位置,

const babel = require("@babel/core")
const traverse = require("@babel/traverse")

const code = ` const a = () => { console.log(1); } `

babel.parse(code, null, (err, ast) => {
  if (err) {
    throw err
  }
  traverse(ast, {
    NumericLiteral(path) {
      console.log(JSON.stringify(path.node, null, 4))
    },
  })
})
複製代碼

因爲console.log(1)接受的參數是一個數字字面量,因此它對應的type就是NumericLiteral。最後找到這個節點的信息以下,

➜  babel node scripts/index.ts
{
    "type": "NumericLiteral",
    "start": 37,
    "end": 38,
    "loc": {
        "start": {
            "line": 3,
            "column": 16
        },
        "end": {
            "line": 3,
            "column": 17
        }
    },
    "extra": {
        "rawValue": 1,
        "raw": "1"
    },
    "value": 1
}
複製代碼

能夠看到,節點包含了 type,loc 信息,以及 value 等信息。更多關於 traverse 的使用,能夠查看這裏Babel#traverse

Plugin

根據上面的思路,咱們能夠得出以下結論,

Babel 中插件接受 AST 做爲參數,而後能夠在 AST 上作一些自定義的處理,最後返回處理以後的 AST。

爲了驗證這個結論正確性,咱們來看看官方的@babel/plugin-transform-arrow-functions的源碼,源碼只有 28 行代碼,我貼出來,並作一些本身的註釋,一塊兒看看。

import { declare } from "@babel/helper-plugin-utils";
import type NodePath from "@babel/traverse";

export default function declare((api, options) {
  // 判斷當前Babel版本是不是v7.x
  api.assertVersion(7);

  // 接受咱們傳入的參數
  const { spec } = options;

  // 返回一個對象
  return {
    name: "transform-arrow-functions",

    visitor: {
      ArrowFunctionExpression(
        path: NodePath<BabelNodeArrowFunctionExpression>,
      ) {
        // 先判斷是否是箭頭函數表達式,不是就直接返回
        if (!path.isArrowFunctionExpression()) return;

        // 將箭頭函數轉爲函數表達式
        path.arrowFunctionToExpression({
          allowInsertArrow: false,
          specCompliant: !!spec,
        });
      },
    },
  };
});
複製代碼

從源碼能夠看出,它返回一個declare函數。這個函數接受兩個參數,一個api,一個是options。函數處理步驟以下,

  1. 判斷是否 Babel v7 的版本
  2. 返回一個對象,包括namevisitor;其中,visitor又是一個對象,它才真正包含對肩頭函數表達式的處理。

實際上,path.arrowFunctionToExpression 就是使用@babel/typesarrowfunctionexpression,詳細能夠查看babel-types#arrowfunctionexpression

跟咱們猜測的 Babel 插件樣子有點出入,可是它包含了咱們猜測的內容。最後,咱們能夠總結出寫一個 Babel 插件的樣子應該是這樣的,

export default function declare(api, options) {
  // api能夠作一些版本兼容性判斷,或者緩存相關的。
  // options就是咱們配置插件時,傳入的參數,這裏插件內部就能夠使用了

  return {
    name: "my-custorm-plugin",
    visitor: {
      // 遍歷AST作處理
    },
  }
}
複製代碼

DIY

清楚了 Babel 插件的模版形式,就能夠按照這個模版寫咱們自定義的功能插件。假設,咱們要寫的一個 Babel 插件,就是去掉全部的console.log相關調試信息的代碼。

// 源代碼
const a = () => {
  console.log(1)
}
複製代碼

例如上面的代碼通過咱們的 Babel 插件處理以後,輸出的代碼應該是一個空的箭頭函數 a,

// 轉換以後
const a = () => {}
複製代碼

根據 Babel 插件模版代碼,咱們能夠這樣實現以下,

// plugins/remove-console-log.js
const types = require("@babel/types")

module.exports = function declare(api, options) {
  api.assertVersion(7)

  return {
    name: "remove-console-log",
    visitor: {
      ExpressionStatement(path) {
        const expression = path.node.expression
        if (types.isCallExpression(expression)) {
          const callee = expression.callee
          if (types.isMemberExpression(callee)) {
            const objName = callee.object.name
            const methodName = callee.property.name
            if (objName === "console" && methodName === "log") {
              path.remove()
            }
          }
        }
      },
    },
  }
}
複製代碼

而後在 babel.config.js 中配置以下,

module.exports = {
  plugins: ["./plugins/remove-console-log.js"],
}
複製代碼

最後,咱們經過 Babel 轉換以後就能夠獲得咱們指望的結果了。

小結

經過本身實現一個 Babel 插件,而後貫穿整個過程把 Babel 原理弄清楚。上面其實還有一個小知識點,就是 Babel 怎麼將源碼轉換成 AST 的。其實,它的過程也不難理解,只是在轉換爲 AST 以前,須要先進行詞法分析,把源碼字符串轉換成 Token 數組;而後根據詞法分析獲得的結果,轉換成 AST。完整的 Babel 原理過程能夠簡單的表述爲以下,

let tokens = tokenizer(input) // 詞法分析
let ast = parser(tokens) // 轉換爲AST
let newAst = transformer(ast) // 調用插件,進行轉換
let output = codeGenerator(newAst) // 最後,生成新的目標代碼
複製代碼

若是想更加詳細研究 Babel 的過程,能夠看看這個簡易的編譯器the-super-tiny-compiler,它實現了完整的流程過程,代碼也很是簡單易懂。

參考

相關文章
相關標籤/搜索