從零開始寫一個 Babel 插件

相信目前常與 ES6 代碼打交道的同學對 Babel 應該不會陌生,在 ES6 代碼被編譯轉化為 ES5 代碼的過程中,Babel 插件顯得尤為重要,我們最后經由 Babel 生成的代碼取決于插件在這一層中做了什么事,在探索這其中的過程之前,我們先來了解下一些所需的基礎知識。

抽象語法樹

Babel 的工作流可以用下面一張圖來表示,代碼首先經由 babylon 解析成抽象語法樹(AST),后經一些遍歷和分析轉換(主要過程),最后根據轉換后的 AST 生成新的常規(guī)代碼。

image.png

在這其中,理解清楚 AST 十分重要,我們之所以需要將代碼轉換為 AST 也是為了讓計算機能夠更好地進行理解。我們可以來看看下面這段代碼被解析成 AST 后對應的結構圖:

function abs(number) {
  if (number >= 0) {  // test
    return number;  // consequent
  } else {
    return -number; // alternate
  }
}
image.png

所有的 AST 根節(jié)點都是 Program 節(jié)點,從上圖中我們可以看到解析生成的 AST 的結構的各個 Node 節(jié)點都很細微,Babylon AST 有個文檔對每個節(jié)點類型都做了詳細的說明,你可以對照各個節(jié)點類型在這查找到所需要的信息。在這個例子中,我們主要關注函數(shù)聲明里的內容, IfStatement 對應代碼中的 if...else 區(qū)塊的內容,我們先對條件(test)進行判斷,這里是個簡單的二進制表達式,我們的分支也會從這個條件繼續(xù)進行下去,consequent 代表條件值為 true 的分支,alternate 代表條件值為 false 的分支,最后兩條分支各自在 ReturnStatement 節(jié)點進行返回。

了解 AST 各個節(jié)點的類型是后續(xù)編寫插件的關緊,AST 通常情況下都是比較復雜的,上述一段簡單的函數(shù)定義也生成了比較大的 AST,對于一些復雜的程序,我們可以借助 astexplorer 來幫我們分析 AST 的結構。

遍歷節(jié)點

在插件里進行節(jié)點遍歷需要先了解 visitor 和 path 的概念,前者相當于從眾多節(jié)點類型中選擇開發(fā)者所需要的節(jié)點,后者相當于對節(jié)點之間的關系的訪問。

visitor

Babel 使用 babel-traverse 進行樹狀的遍歷,對于 AST 樹上的每一個分支我們都會先向下遍歷走到盡頭,然后向上遍歷退出遍歷過的節(jié)點尋找下一個分支。Babel 提供我們一個 visitor 對象供我們獲取 AST 里所需的具體節(jié)點來進行訪問,比如我只想訪問 if...else 生成的節(jié)點,我們可以在 visitor 里指定獲取它所對應的節(jié)點:

const visitor = {
  IfStatement() {
    console.log('get if');
  }
};

繼續(xù)上述所說的遍歷,其實這種遍歷會讓每個節(jié)點都會被訪問兩次,一次是向下遍歷代表進入(enter),一次是向上退出(exit)。因此實際上每個節(jié)點都會有 enter 和 exit 方法,在實際操作的時候需要注意這種遍歷方式可能會引起的一些問題,上述例子是省略掉 enter 的簡寫。

const visitor = {
  IfStatement: {
    enter() {},
    exit() {}
  }
}

path

visitor 模式中我們對節(jié)點的訪問實際上是對節(jié)點路徑的訪問,在這個模式中我們一般把 path 當作參數(shù)傳入節(jié)點選擇器中。path 表示兩個節(jié)點之間的連接,通過這個對象我們可以訪問到節(jié)點、父節(jié)點以及進行一系列跟節(jié)點操作相關的方法(類似 DOM 的操作)。

var babel = require('babel-core');
var t = require('babel-types');

const code = `d = a + b + c`;

const visitor = {
    Identifier(path) {
        console.log(path.node.name);  // d a b c
    }
};

const result = babel.transform(code, {
    plugins: [{
        visitor: visitor
    }]
});

替換節(jié)點

具備了 AST 相關知識和了解 visitor、path 后,就可以編寫一個簡單的 Babel 插件了。我們要把上述的 abs 函數(shù)換成原生支持的 Math.abs 來進行調用 。

首先我們先解析下 abs(-8) 的 AST 結構,直接從表達式語句(ExpressionStatement)開始:

{
  type: "ExpressionStatement",
  expression: {
    type: "CallExpression",
    callee: {
      type: "Identifier",
      name: "abs"
    },
    arguments: [{
      type: "UnaryExpression",
      operator: "-",
      prefix: true,
      arguments: {
        type: "NumericLiteral",
        value: 8
      }
    }]
  }
}

上述AST結構可以在 astexplorer 查看。

我們可以看到表達式語句下面的 expression 主要是函數(shù)調用表達式(CallExpression),因此我們也需要創(chuàng)建一個函數(shù)調用表達式,此外,Math.abs 是一個二元操作表達式,屬于 MemberExpression 類型。上述兩個 AST 節(jié)點我們可以借助 babel-types 里提供的一些方法幫我們快速創(chuàng)建。

// 創(chuàng)建函數(shù)調用表達式
t.CallExpression(
    // 創(chuàng)建對象屬性引用
    t.MemberExpression(t.identifier('Math'), t.identifier('abs')), 
    // 原始節(jié)點函數(shù)調用參數(shù)
    path.node.arguments 
)

最后我們需要對此次函數(shù)調用不符合的節(jié)點進行過濾,過濾掉名字不等于 abs 的函數(shù)調用,因為 Babel 在遍歷的過程是遞歸的,如果不過濾做限制的話,程序將會一直運行最終報調用棧超過閾值的錯誤。

RangeError: unknown: Maximum call stack size exceeded

最終代碼如下:

var babel = require('babel-core');
var t = require('babel-types');
const code = `abs(-8);`;

const visitor = {
    CallExpression(path) {
        if (path.node.callee.name !== 'abs') return;
        
        path.replaceWith(t.CallExpression(
            t.MemberExpression(t.identifier('Math'), t.identifier('abs')),
            path.node.arguments
        ));
    }
};

const result = babel.transform(code, {
    plugins: [{
        visitor: visitor
    }]
});

// Math.abs(-8)
console.log(result.code);

上述例子使用了 transform api 直接解析轉換生成了新的代碼,另外在單獨編寫 Babel 插件的時候,暴露的參數(shù)里一般都含有常用的 babel-types 對象供使用。

export default function({ types: t }) {
    return {
        visitor: {
            CallExpression(path) {
                if (path.node.callee.name !== 'abs') return;

                path.replaceWith(t.CallExpression(
                    t.MemberExpression(t.identifier('Math'),
                    t.identifier('abs')),
                    path.node.arguments
                ));
            }
        }
    };
}

神奇,可以在線直觀的觀察插件是否起作用!

參考文章:

Webpack4+Babel7優(yōu)化70%速度
babel-handbook

從零開始編寫一個babel插件

webpack編譯原理
babel入門指南
babel編譯原理
AST
babel-types
babel-plugin-handbook
如何上傳一個npm包

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容