作為當今最為常用的 JavaScript 編譯器,Babel 在前端開發中扮演著極為重要的角色。大多數情況下,Babel 被用來轉譯 ECMAScript 2015+ 至可相容瀏覽器的版本。然而作為一款功能強大的 JavaScript 編譯器,Babel 值得我們探索的遠不止於此。本文將以 Babel 的核心之一外掛為切入點,探尋在開發實踐中除了轉譯 ES6,Babel 外掛到底還有啥用?

關於 Babel 外掛

Babel 編譯程式碼的過程可分為三個階段:解析(parsing)、轉換(transforming)、生成(generating),這三個階段分別由 @babel/parser、@babel/core、@babel/generator 執行。Babel 本質上只是一個程式碼的搬運工,如果不給 Babel 裝上外掛,它將會把輸入的程式碼原封不動地輸出。正是因為有外掛的存在, Babel 才能將輸入的程式碼進行轉變,從而生成新的程式碼。

Babel 外掛大致分為兩種:語法外掛(syntax plugin)和轉換外掛(transform plugin):語法外掛作用於 @babel/parser,負責將程式碼解析為抽象語法樹(AST)(官方的語法外掛以 babel-plugin-syntax 開頭);轉換外掛作用於 @babel/core,負責轉換 AST 的形態(官方的轉換外掛以 babel-plugin-transform(正式)或 babel-plugin-proposal(提案)開頭)。

語法外掛雖名為外掛,但其本身並不具有功能性。語法外掛所對應的語法功能其實都已在 @babel/parser 裡實現,外掛的作用只是將對應語法的解析功能開啟。所以下文提及的 Babel 外掛將專指轉換外掛。

Babel 外掛擔負著編譯過程中的核心任務:轉換 AST。AST 是一個有著多層巢狀的樹狀結構,理論上講寫一個外掛改變 AST 並不是什麼難事。但想要快速便捷得完成外掛的開發,則需要藉助以下幾樣工具。

traverse

@babel/traverse 是一款用來自動遍歷抽象語法樹的工具,它會訪問樹中的所有節點,在進入每個節點時觸發 enter 鉤子函式,退出每個節點時觸發 exit 鉤子函式。開發者可在鉤子函式中對 AST 進行修改。

import

traverse

from

“@babel/traverse”

traverse

ast

{

enter

path

{

// 進入 path 後觸發

},

exit

path

{

// 退出 path 前觸發

},

});

types

@babel/types 是一款作用於 AST 的類 lodash 庫,其封裝了大量與 AST 有關的方法,大大降低了轉換 AST 的成本。@babel/types 的功能主要有兩種:一方面可以用它驗證 AST 節點的型別,例如使用 isClassMethod 或 assertClassMethod 方法可以判斷 AST 節點是否為 class 中的一個 method;另一方面可以用它構建 AST 節點,例如呼叫 classMethod 方法,可生成一個新的 classMethod 型別 AST 節點 。

template

@babel/template 實現了計算機科學中一種被稱為準引用(quasiquotes)的概念。說白了,它能直接將字串程式碼片段(可在字串程式碼中嵌入變數)轉換為 AST 節點。例如下面的例子中,@babel/template 可以將一段引入 axios 的宣告直接轉變為 AST 節點。

import

template

from

“@babel/template”

const

ast

=

template

ast

`

const axios = require(“axios”);

`

);

Babel 外掛的諸多用處

既然 Babel 外掛有著如此豐富的功能,那我們當然不能只滿足於用 Babel 轉譯 ES6 。其實在開發實踐的許多場景中,藉助 Babel 外掛能夠自由轉換程式碼的優勢,我們可以在編譯程式碼後大大最佳化程式碼的質量,並提高開發效率。

接下來,我將分別從擴充套件既有方法、提前執行執行時程式碼、提高程式碼效能等三個角度來探索如何在實踐中高效利用 Babel 外掛。

擴充套件既有方法

在開發 node 應用程式特別是 node cli 應用時,我們經常需要在終端裡用 console。log 打印出各種各樣的文案。列印文案會更加便於監測程式的執行,但當整個程式中 console。log 較多且散落在各個檔案中時,開發者可能很難快速找出螢幕上的文案是由哪個檔案裡的那一行程式碼列印的。 想要快速定位到 console。log 被呼叫的位置,較為粗暴的方式是使用 console。trace,console。trace 會把 trace 路徑在螢幕上一併打印出來。但 console。trace 顯然不適合在生產環境使用,在生產環境使用之將極大地損傷列印內容的可讀性。要想讓開發環境的 log 顯示出 trace 資訊而生產環境的不顯示,只要在開發環境程式碼的編譯過程中用 Babel 外掛為console。log 新增 trace 功能即可。

// test。js

console

log

‘Where am I from?’

);

執行上面的 test。js 檔案後,毫無疑問,螢幕上只會出現一句孤零零的文案。要想加上 trace 資訊,我們得先把這句程式碼進行解剖分析,看看如何才能將其改頭換面

首先使用 @babel/parser 將程式碼解析成如下 AST

Babel 外掛有啥用?

從 AST 中我們可以看出,

console。log(‘Where am I from?’)

這行程式碼是一個MemberExpression 節點,它由 object、property、arguments 等三個子節點組成。展開 arguments,可以發現它包含著一個 value 為 「Where am I from?」 的 StringLiteral,且其中已經標出了它的起始位置line 1, column 12。所以只需將位置資訊插入到 value 當中即可在 log 的時候顯示出 trace 資訊。

// plugin。js

import

traverse

from

“@babel/traverse”

import

t

from

“@babel/types”

traverse

ast

{

enter

path

{

if

t

isStringLiteral

path

node

&&

滿足console

log引數節點的其他條件

{

const

location

=

`line

${

path

node

loc

start

line

}

, column

${

path

node

loc

start

column

}

`

path

node

value

=

`

${

path

node

value

}

(trace:

${

location

}

)`

}

}

});

上面的程式碼對 AST 進行了遍歷,當訪問到 console。log 引數所在的 StringLiteral 節點時,先將該節點的位置資訊取出,然後將位置資訊插入到引數的 value 當中去。用此外掛對程式碼進行編譯後,console。log 的功能將得到擴充套件:不僅能夠輸出 log 方法的引數值,且能將 console。log 引數在原始檔中的位置一併輸出。

提前執行執行時程式碼

let

result

=

{

country

‘China’

capital

‘Beijing’

},

{

country

‘Japan’

capital

‘Tokyo’

},

{

country

‘Russia’

capital

‘Moscow’

},

{

country

‘France’

capital

‘Paris’

},

]。

map

e

=>

‘The capital of’

+

e

country

+

‘is’

+

e

capital

);

上面這段程式碼透過 map 方法處理了一個由物件元素組成的靜態陣列,生成了一個由字串元素組成的陣列。由於這段程式碼中沒有動態變數,所以放到任何一個使用者的瀏覽器裡去執行,都會生成同樣的結果。在瀏覽器或其他客戶端的執行時環境裡執行這段程式碼,無疑是一種不必要的消耗。但如果開發者在程式碼中直接將變數 result 寫成一個由字串組成的陣列,會大大降低開發的便捷性。既不想在執行時執行,又不願意在開發時寫死,那只有藉助 Babel 在編譯時去執行這段程式碼了。

為了讓賦值語句的右值能夠在編譯時被預處理,我們可以在 Array 的 map 方法外面套一個用來標記用的 calc 方法,以此來告知 Babel 需要在編譯時執行這段程式碼。

// test。js

let

result

=

calc

`[

{

country: ‘China’,

capital: ‘Beijing’

},

{

country: ‘Japan’,

capital: ‘Tokyo’

},

{

country: ‘Russia’,

capital: ‘Moscow’

},

{

country: ‘France’,

capital: ‘Paris’

},

]。map(e => ‘The capital of’ + e。country + ‘is’ + e。capital)`

);

使用 @babel/parser 對 test。js 進行處理,會得到如下 AST

Babel 外掛有啥用?

從 AST 中可以看出,整段賦值的程式碼是一個 VariableDeclarator,等號的左側是一個 name 為 result 的 Identifier,右側是一個 CallExpression。再展開 CallExpression 看看裡面有什麼

Babel 外掛有啥用?

展開 CallExpression 的 arguments,可以發現 value 裡以字串的形式完整記錄了對陣列進行 map 操作的程式碼。頓時局勢變得明朗起來:只需要計算出 map 方法的結果,並用該結果替換等號右側的 CallExpression 即可。

// plugin。js

import

traverse

from

“@babel/traverse”

import

t

from

“@babel/types”

import

template

from

“@babel/template”

let

rawCode

traverse

ast

{

enter

path

{

if

t

isTemplateElement

path

node

&&

滿足其他條件

{

rawCode

=

path

node

value

raw

}

}

});

traverse

ast

{

enter

path

{

if

t

isVariableDeclarator

path

node

&&

t

isIdentifier

path

node

{

name

‘result’

}))

{

path

node

init

needReplaced

=

true

}

if

path

node

needReplaced

{

const

buildRequire

=

template

`RAW_CODE`

);

const

builtAST

=

buildRequire

({

RAW_CODE

eval

rawCode

});

path

replaceWith

builtAST

);

}

}

});

上面的外掛程式碼中,先透過遍歷 AST 找到 TemplateElement 節點,從 TemplateElement 節點中取出字串格式的 map 方法程式碼。接下來在訪問到 VariableDeclarator 節點的時候,使用 eval 方法計算出字串程式碼的結果(一個由字串元素組成的陣列),最後用 @babel/template 將計算出的陣列轉為 AST 節點,替換賦值語句等號右側的 CallExpression。至此,一個原本需要在執行時執行的 map 方法已在編譯時提前計算出了結果。

提高程式碼效能

在程式的開發過程中,程式碼的高效能和開發的便捷性一直是一對難以共存的矛盾體。例如要對一個數組進行遍歷,有 for、forEach、map 等許多方式可供選擇。若選擇了 for 迴圈,將無法體驗 forEach、map 等 Array 方法的便捷功能;若選擇了 Array 方法,將面臨更高的效能開支(因為 Array 方法除了迴圈以外還需要執行其他許多工,如考慮上下文、考慮稀疏陣列、生成新陣列等,其效能註定無法超越 for 迴圈)。

arr

forEach

e

=>

console

log

e

));

上面是一個簡單的 forEach 方法,想要提高效能,我們必然會想到將程式碼寫成這樣:

for

let

i

=

0

i

<

arr

length

i

++

{

console

log

arr

i

]);

}

為了既保證程式碼的高效能,又保留開發的便捷性,可以在編譯時用 Babel 外掛將 forEach 轉換為 for 迴圈。由於 forEach 箭頭函式 body 中的內容與 for 迴圈 body 中的內容大致相同,所以在轉換 AST 時,只需將 forEach 箭頭函式的 body 節點移植到 for 迴圈的 body 節點並修改一些變數名即可。鑑於 forEach 轉換成 for 迴圈的過程中,需要考慮的特殊情況較多,在此就不詳細描述轉換過程了。如果想在開發實踐中將程式碼中的 Array 方法全部替換成 for 迴圈以提高效能,可以使用現成的 Babel 外掛 faster。js。

最後想說的

雖然 Babel 問世之時被命名為 6to5,但它如今已不只是一款僅能將 ES6 轉為 ES5 的前端工具,藉助 Babel 外掛的力量,我們在 JavaScript 的世界裡還有著非常巨大的想象空間。

本文只介紹瞭如何編寫 Babel 轉換外掛,其實縱觀整個 Babel 生態,還有非常多的事情可做,例如修改 @babel/parser 可以給 JavaScript 新增自定義語法,換一個 generator 可以將 JavaScript 編譯成另外某種語言等等。

從明天起,做一個幸福的人,不要再拘泥於 Babel 已有的功能。Babel 外掛有啥用?官網上 Babel 功能列表的最後一條寫得明明白白:And more!