開發一個簡單的babel插件

前言

對於前端開發而言,babel確定是再熟悉不過了,工做中確定會用到。除了用做轉換es6和jsx的工具以外,我的感受babel基於抽象語法樹的插件機制,給咱們提供了更多的可能。關於babel相關概念和插件文檔,網上是有不少的,講的挺不錯的。詳細的解析推薦官方的babel插件手冊。在開發插件以前,有些內容仍是要了解一下的,已經熟悉的大佬們能夠直接跳過。
html

抽象語法樹(AST)

Babel 使用一個基於 ESTree 並修改過的 AST,它的內核說明文檔能夠在這裏找到。
直接看實例應該更清晰:前端

function square(n) {
  return n * n;
}

對應的AST對象(babel提供的對象格式)node

{
  //代碼塊類別,函數聲明
  type: "FunctionDeclaration",
  //變量標識
  id: {
    type: "Identifier",
    //變量名稱
    name: "square"
  },
  //參數
  params: [{
    type: "Identifier",
    name: "n"
  }],
  //函數體
  body: {
     //塊語句
    type: "BlockStatement",
    body: [{
       //return 語句
      type: "ReturnStatement",
      argument: {
        //二元表達式
        type: "BinaryExpression",
        //操做符
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

大概就是上面這個層級關係,每一層都被稱爲節點(Node),一個完整AST對應的js對象可能會有不少節點,視具體狀況而定。babel將每一個節點都做爲一個接口返回。其中包括的屬性就如上面代碼所示,例如type,start,end,loc等通用屬性和具體type對應的私有屬性。咱們後面插件的處理也是根據不一樣的type來處理的。git

看到這個龐大的js對象,不要感到頭疼,若是說讓咱們每次都本身去分析AST和按照babel的定義去記住不一樣類型,顯然不現實。這種事情應該交給電腦來執行,咱們能夠利用AST Explorer來將目標代碼轉成語法樹對象,結合AST node types來查看具體屬性。es6

Babel 的處理步驟

Babel 的三個主要處理步驟分別是: 解析(parse),轉換(transform),生成(generate),具體過程就不想詳細描述了,直接看官方手冊就好。
須要注意的是,babel插件就是在轉換過程當中起做用的,即將解析完成的語法樹對象按照本身的目的進行處理,而後再進行代碼生成步驟。因此要深刻了解轉換相關的內容。github

代碼生成步驟把最終(通過一系列轉換以後)的 AST 轉換成字符串形式的代碼,同時還會建立源碼映射(source maps),以便於調試。算法

代碼生成的原理:深度優先遍歷整個 AST,而後構建能夠表示轉換後代碼的字符串。轉換的時候是是進行遞歸的樹形遍歷。api

轉換

Visitor

轉換的時候,是插件開始起做用的時候,可是如何進入到這個過程呢,babel給咱們提供了一個Visitor的規範。咱們能夠經過Visitor來定義咱們的訪問邏輯。大概就是下面這個樣子數組

const MyVisitor = {
  //這裏對應上面node的type,全部type爲Identifier的節點都會進入該方法中
  Identifier() {
    console.log("Called!");
  }
};
//以該方法爲例 
function square(n) {
  return n * n;
} 
//會調用四次,由於
//函數名square
//形參 n
//函數體中的兩個n,都是Identifier
path.traverse(MyVisitor); 
//  因此輸出四個
Called!
Called!
Called!
Called!

由於深度優先的遍歷算法,到一個葉子節點以後,發現沒有子孫節點,須要向上溯源才能回到上一級繼續遍歷下個子節點,因此每一個節點都會被訪問兩次。
若是不指定的話,調用都發生在進入節點時,固然也能夠在退出時調用訪問者方法。babel

const MyVisitor = {
  Identifier: {
    enter() {
      console.log("Entered!");
    },
    exit() {
      console.log("Exited!");
    }
  }
};

此外還有一些小技巧:

能夠在方法名用|來匹配多種不一樣的type,使用相同的處理函數。

const MyVisitor = {
  "ExportNamedDeclaration|Flow"(path) {}
};

此外能夠在訪問者中使用別名(如babel-types定義)
例如Function是FunctionDeclaration,FunctionExpression,ArrowFunctionExpression,ObjectMethod和ObjectMethod的別名,能夠用它來匹配上述全部類型的type

const MyVisitor = {
  Function(path) {}
};

Paths

AST 一般會有許多節點,那麼節點直接如何相互關聯呢? 咱們可使用一個可操做和訪問的巨大可變對象表示節點之間的關聯關係,或者也能夠用Paths(路徑)來簡化這件事情。Path 是表示兩個節點之間鏈接的對象。直接看例子比較清晰一點。

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  ...
}

將子節點 Identifier 表示爲一個路徑(Path)的話,看起來是這樣的:

{
  "parent": {
    "type": "FunctionDeclaration",
    "id": {...},
    ....
  },
  "node": {
    "type": "Identifier",
    "name": "square"
  }
}

當你經過一個 Identifier() 成員方法的訪問者時,你其實是在訪問路徑而非節點。 經過這種方式,你操做的就是節點的響應式表示(譯註:即路徑)而非節點自己。

編寫插件

前面都是些必備知識點,本文只是將一些相對重要一點的知識點提了一下。詳細的仍是要去看開發手冊的。
我的而言開發插件的話應該有下面三個步驟:

  1. 分析源文件抽象語法樹AST
  2. 分析目標文件抽象語法樹
  3. 構建Visitor
    3.1 肯定訪問條件
    3.2 肯定轉換邏輯

插件主要的就是3步驟,可是前兩步是十分重要的。3.1和3.2分別依賴於1和2的結果。只有清晰瞭解AST結構以後,纔能有的放矢,事半功倍。
舉個例子,以下代碼:

var func = ()=>{
    console.log(this.b)
};

目的是將箭頭函數轉換成普通函數聲明(這裏僅僅是具體這種格式的轉化,其餘部分就先不涉及)。以下:

var _this = this;
var func = function () {
    console.log(_this.b);
};

源文件語法樹

這裏分析下這個簡單的函數聲明,按照上面定義分析,不過這裏仍是推薦AST Explorer能夠清晰的看到咱們的語法樹。這裏只截取有用信息:

"init": {
              "type": "ArrowFunctionExpression",
              /*...其餘信息....*/
              "id": null,
              //形參
              "params": [],
              "body": {
                //函數體,this部分
                "arguments": [
                        {
                          "type": "MemberExpression",
                          "object": {
                             //this 表達式
                            "type": "ThisExpression",
                          },
                          "property": {
                             //b屬性
                            "type": "Identifier",
                            "name": "b"
                          }
                        }
                      ]
             }
        }

咱們要轉換的只是ArrowFunctionExpression即箭頭函數和this表達式ThisExpression部分,其餘暫時不動。
那麼咱們的visitor裏的函數名稱就包括ArrowFunctionExpression和ThisExpression了。

//visitor裏面方法的key就對應咱們要處理的node  type
const visitor = {
    //處理this表達式  
    ThisExpression(path){
        //將this轉換成_this的形式
    },
    //處理箭頭函數。
    ArrowFunctionExpression(path){
       //轉換成普通的FunctionExpression
    }
}

目標文件語法樹

一樣的方法,語法樹對象以下:
語法樹太長,咱們就看一下變化的地方好了

//轉換以後的body由兩個元素的數組,兩個變量聲明是統計關係
    "body": [
      //var _this = this;結構
      {
        "type": "VariableDeclaration",
        "kind": "var",
        "declarations": [
          {
            "type": "VariableDeclarator",
            //left爲_this的標識
            "id": {
              "type": "Identifier",
              "name": "_this"
            },
            //right爲this表達式
            "init": {
              "type": "ThisExpression"
              /***其餘**/
            }
      },   
      // var func = function (b) {
      //      console.log(_this.b);
      //  };結構 只看關鍵的
      {
        "type": "VariableDeclaration",
        "kind": "var",
        "declarations": [
          {
            /*****省略*******/
            "arguments": [
                        {
                          "type": "MemberExpression",
                          //轉換以後的_this.b
                          "object": {
                            "type": "Identifier",
                            "name": "_this"
                          },
                          "property": {
                            "type": "Identifier",
                            "name": "b"
                          }
                          ]
          }
      }
    ]

通過對比,肯定咱們的操做應該是將ArrowFunctionExpression替換爲FunctionExpression,遇到有this表達式的,綁定一下this,並將其轉換。
進行替換增長等操做時就要用到path提供的api了:

  • replaceWith(targetObj) 替換
  • findParent() 查找知足條件的父節點
  • insertBefore 插入兄弟節點
    更多請查詢文檔,這裏只列出咱們用到的方法。

構造節點

這裏將這個操做單獨拿出來,toFunctionExpression這個api的說明我始終沒找到。。。。多是我沒找對地方FunctionExpression,沒辦法我去babel源碼裏找了一遍:

//@src  /babel/packages/babel-types/src/definitions/core.js
defineType("FunctionExpression", {
  inherits: "FunctionDeclaration",
  //....
}
//又找到 FunctionDeclaration
defineType("FunctionDeclaration", {
  //這裏纔看到參數: id,params,body..
  builder: ["id", "params", "body", "generator", "async"],
  visitor: ["id", "params", "body", "returnType", "typeParameters"]
  //....  
}

這樣的話才知道入參,若是有清晰的文檔,請你們不吝賜教。下面就簡單了。

後來又專門找了一下,終於找到對應文檔了傳送門

完善Visitor

const Visitor = {
    //this表達式
    ThisExpression(path){
        //構建var _this = this
        let node = t.VariableDeclaration(
            'var',
            [
                t.VariableDeclarator(
                    t.Identifier('_this'),
                    t.Identifier('this')
                )
            ]
        ),
        //構建 _this標識符
        str = t.Identifier('_this'),
        //查找變量聲明的父節點
        //這裏只是針對例子的,真正轉換須要考慮的狀況不少
        parentPath = path.findParent((path) => path.isVariableDeclaration())
        //知足條件
        if(parentPath){
            //插入
            parentPath.insertBefore(node)
            path.replaceWith(
                str
            )
        }else{
            return
        }
    },
    //處理箭頭函數。
    ArrowFunctionExpression(path){
        var node = path.node
        //構造一個t.FunctionExpression節點,將原有path替換掉便可
        path.replaceWith(t.FunctionExpression(
            node.id,
            node.params,
            node.body
          ))
    }
}

主體visitor至此算結束了,固然若是是插件的話

//babel調用插件時會將babel-types做爲參數傳入 
export default function({ types: t }) {
  return {
    visitor:Visitor
  }

在本地調試的話,能夠分別引入babel-core和babel-types

var babel = require('babel-core');
var t = require('babel-types');
var code = `var func = ()=>{
    console.log(this.b)
  };`
const result = babel.transform(code, {
    plugins: [{
      //前面的Visitor
        visitor: Visitor
    }]
});  
//輸出轉換以後的code
/**
 * var _this = this;
 * var func = function () {
 * console.log(_this.b);
 * }; 
 */
console.log(result.code);

結束語

參考文章

Babel 插件手冊
Babel for ES6? And Beyond!

紙上得來終覺淺,本來也認爲已經理解了babel的原理和插件機制,沒想到連寫個小demo都這麼費勁。主要仍是對相關api不熟悉,不知道如何去構建節點,熟練以後應該會好不少。此文是插件手冊的一個簡單總結,把本身實現的思路彙總了一下。拋磚引玉,共同進步,另外但願對有須要的同窗略有幫助。詳見個人博客

相關文章
相關標籤/搜索