探索 babel 和 babel 外掛是怎麼工作的

發表於2018-03-05
你有可能會聽到過這個詞 webpack工程師 ,這個看似像是一個專業很強的職位其實很多時候是一些前端對現在前端工作方式對一些吐槽,對於一個之前沒有接觸過webpacknodejs,babel 之類的工具的人來說,看到大量的配置檔案後很多人都會看懵config-hurt-my-heart

很多人就乾脆不管這些東西,直接上手寫業務程式碼,把這些構建工具就相當於黑科技,我們把所有的檔案都經過這些工具最終生成一個或者幾個打包後的檔案,其中關於優化和程式碼轉換問題其實一大部分都是在這些配置裡面的。如果我們不去了解其中的一部分原理,後面遇到很多問題(如打包後檔案體積過大)時候都是束手無策,而且萬一哪天構建工具出現問題時候可能連工作都開展不下去了。

既然我們日常都要用到,最好的方式就是去研究一下這些工具的原理的作用,讓這些工具成為我們手中的利器,而不是工作上的絆腳石,而且這些工具的設計者都是頂級的工程師,當你敲開壁壘探究內部祕密時候,我相信你會感受到其中的程式設計之美。

這裡我們去探索一下babel的原理

babel 是什麼?

Babel · The compiler for writing next generation JavaScript

6to5

你在npm上可以看到這樣一個包名字是6to5, 光看名字可能會讓人感覺到很詫異,名字看起來可能有點奇怪,其實babel 在開始的時候名字就是這個。簡單粗暴es6 -> es5,一下子就看懂了babel 是用來幹啥的,但是很明顯這不是一個好名字,這個名字會讓人感覺到es6普及之後這個庫就沒用了,為了保持活力這個庫可能要不停的修改名字。下面是babel作者一次分享中假設如果按這個命名法則可能出現的名稱

babel-name-history

很明顯發生這種情況是很不合理的,團隊內部經過大量討論後,最終選擇了babel,這與電影銀河系漫遊指南中的Babel fish相應,也有關係到聖經中的一個故事Tower of Babel(ps.優秀的人總是也很有情懷。)

babel is the new jQuery

redux 的作者曾說過這樣一句話,可以換一種理解為

babel 對於 AST 就相當於 jQuery 對於 DOM, 就是說babel給予了我們便捷查詢和修改 AST 的能力。(AST -> Abstract Syntax Tree) 抽象語法樹 後面會講到。

什麼要用babel轉換程式碼

我們之前做一些相容都會都會接觸一些 Polyfill 的概念,比如如果某個版本的瀏覽器不支援 Array.prototype.find 方法,但是我們的程式碼中有用到Arrayfind 函式,為了支援這些程式碼,我們會人為的加一些相容程式碼

對於這種情況做相容也很好實現,引入一個 Polyfill 檔案就可以了,但是有一些情況我們使用到了一些新語法,或者一些其他寫法

這種情況靠 Polyfill, 因為一些瀏覽器根本就不識別這些程式碼,這時候就需要把這些程式碼轉換成瀏覽器識別的程式碼。babel就是做這個事情的。

babel做了哪些事情

babel-core

為了轉換我們的程式碼,babel做了三件事

  • Parser 解析我們的程式碼轉換為AST
  • Transformer 利用我們配置好的plugins/presetsParser生成的AST轉變為新的AST
  • Generator 把轉換後的AST生成新的程式碼從圖上看 Transformer 佔了很大一塊比重,這個轉換過程就是babel中最複雜的部分,我們平時配置的plugins/presets就是在這個模組起作用。

從簡單的說起

可以看到要想搞懂babel, 就是去了解上面三個步驟都是在幹什麼,我們先把比較容易看懂的地方開始瞭解一下。

Parser 解析

解析步驟接收程式碼並輸出 AST,這其中又包含兩個階段詞法分析語法分析。詞法分析階段把字串形式的程式碼轉換為 令牌(tokens) 流。語法分析階段會把一個令牌流轉換成 AST 的形式,方便後續操作。

Generator 生成

程式碼生成步驟把最終(經過一系列轉換之後)的 AST 轉換成字串形式的程式碼,同時還會建立原始碼對映(source maps)。程式碼生成其實很簡單:深度優先遍歷整個 AST,然後構建可以表示轉換後程式碼的字串。

babel的核心內容

看起來babel的主要工作都集中在把解析生成的AST經過plugins/presets然後去生成新的AST這上面了。

AST抽象語法樹

我們一直在提到AST它究竟是什麼呢,既然它的名字叫做抽象語法樹,我們可以想象一下如果把我們的程式用樹狀表示會是什麼樣呢。

我們想象一下要表示上述程式碼應該是什麼樣子,首先必須有東西可以表示這些具體的宣告,變數,常量的具體資訊,比如(這棵樹上肯定有二個變數,變數名是a和b,肯定有兩個運算語句,操作符是 + ),有了這些資訊還不夠,我們必須建立起它們之間的關係,比如一個宣告語句,宣告型別是 var, 左側是變數, 右側是表示式。有了這些資訊我們就可以還原這個程式,這也是把程式碼解析成AST時候所做的事情,對應上面我們說的詞法分析語法分析

AST中我們用node(節點)來表示各個程式碼片段,比如我們上面程式整體就是一個節點Program節點(所有的 AST 根節點都是 Program 節點),因為它下面有兩條語句所以它的 body屬性上就兩個宣告節點VariableDeclaration。所以上面程式的AST就類似這樣

ast

可以看到在節點上用各個的屬性去表示各種資訊以及程式之間的關係,那這些節點每一個叫什麼名字,都用哪些屬性名呢?我們可以在說明文件上找到這些說明。

關於介面

看這個文件時候我們可以看到說明大多是類似這種

這裡提到interface這個我們在其他語言中是比較常見的,比如Node規定了typeloc屬性,如果其他節點繼承自Node,那麼它也會實現typeloc屬性就是說繼承自Node的節點也會有這些屬性,基本所有節點都繼承自Node,所以我們基本可以看到loc這個屬性loc表示個一些位置資訊。

節點單位

我們程式很多地方都會被拆分成一個個的節點,節點裡面也會套著其他的節點,我們在文件中可以看到AST結構的各個 Node 節點都很細微,比如我們宣告函式,函式就是一個節點FunctionDeclaration,函式名和形參那麼引數都是一個變數節點Identifier。生成的節點往往都很複雜,我們可以藉助astexplorer來幫助我們分析AST結構。

影像展示

有了上面這些概念我們已經可以大概瞭解AST的概念,以及各個模組代表的含義,假設我們有這樣一個程式,我們用圖形簡易的分析下它的結構

ast-example

節點遍歷

經過一番努力我們終於瞭解了AST以及其中內容的含義,但是這一部分基本不需要我們做什麼,babel會藉助Babylon幫我們生成我們需要的AST結構。我們更多要去做的是去修改和改變Babylon生成的這個抽象語法樹。

babel拿到抽象語法樹後會使用babel-traverse進行遞迴的樹狀遍歷,對於每一個節點都會向下遍歷到盡頭,然後向上遍歷退出分支去尋找下一個分支。這樣確保我們能找到任何一個節點,也就是能訪問到我們程式碼的任何一個部分。可是我們要怎麼去完成修改操作呢,babel給我們提供了下面這兩個概念。

visitor

我們已經知道babel會遍歷節點組成的抽象語法樹,每一個節點都會有自己對應的type,比如變數節點Identifier等。我們需要給babel提供一個visitor物件,在這個物件上面我們以這些節點的type做為key,已一個函式作為值,類似如下,

這樣在遍歷進入到對應到節點時候,babel就會去執行對應的enter函式,向上遍歷退出對應節點時候,babel就會去執行對應的exit函式,接著上面的程式碼我們可以做一個測試

我們執行對應程式碼可以看到上面enterexit函式分別執行了四次

從上面簡單的程式碼上也可以看到a,b,c,d四個變數,它們應該屬於同一級別的節點樹上,所以遍歷時候會分別進入對應節點然後退出再去下一個節點。

Paths

我們通過visitor可以在遍歷到對應節點執行對應的函式,可是要修改對應節點的資訊,我們還需要拿到對應節點的資訊以及節點和所在的位置(即和其他節點間的關係), visitor在遍歷到對應節點執行對應函式時候會給我們傳入path引數,輔助我們完成上面這些操作。注意 Path 是表示兩個節點之間連線的物件,而不是當前節點,我們上面訪問到了Identifier節點,它傳入的 path引數看起來是這樣的

從上面我們可以看到 path 表示兩個節點之間的連線,通過這個物件我們可以訪問到節點、父節點以及進行一系列跟節點操作相關的方法。我們修改一下上面的 visitor 函式

在執行一下上面的程式碼就可以看到name列印出來的依次是a,b,c,d。這樣我們就有可以修改操作我們需要改變的節點了。另外path物件上還包含新增、更新、移動和刪除節點有關的其他很多方法,我們可以通過文件去了解。

一些有用的工具

babel為了方便我們開發,在每一個環節都有很多人性化的定義也提供了很多實用性的工具,比如之前我們在定義visitor時候分別定義了enter,exit函式,可很多時候我們其實只用到了一次在enter的時候做一些處理就行了。所以我們如果我們直接定義節點的key為函式,就相當於定義了enter函式

上面我們還提到了plugins是函式的情況,其實我們寫的差距一般都是一個函式,這個入口函式上babel也會穿入一個babel-types,這是一個用於AST 節點的 Lodash 式工具庫(類似lodash對於js的幫助), 它包含了構造、驗證以及變換 AST 節點的方法。 該工具庫包含考慮周到的工具方法,對編寫處理AST邏輯非常有用。

實際運用

假如我們有如下程式碼

我們發現這裡把console.log簡寫成了log,為了讓這些程式碼可以執行,我們現在用babel裝置去轉換一下這些程式碼。

改變log函式呼叫本身

既然是console.log沒有寫全,我們就改變這個log函式呼叫的地方,把每一個log替換成console.log,我們看一下log(*)屬於函式執行語句,相對應的節點就是CallExpression,我們看下它的結構

callee是我們函式執行的名稱,arguments就是我們穿入的引數,引數我們不需要改變,只需要把函式名稱改變就好了,之前的callee是一個變數,我們現在要把它變成一個表示式(取物件屬性值的表示式),我們看一下手冊可以看到是一個MemberExpression型別的值,這裡也可以藉助之前提到的網站astexplorer來幫助我們分析。有了這些資訊我們就可以去實現我們的目的了,我們這裡手動引入一下babel-types輔助我們建立新的節點

執行後我們可以看到結果

直接在模組中宣告log

我們已經知道每一個模組都是一個對於的AST,而AST根節點是 Program 節點,下面的語句都是body上面的子節點,我們只要在body頭宣告一下log變數,把它定義為console.log,後面這樣使用就也正常了。

這裡簡單的修改下visitor

執行後生成的程式碼為

總結

到這裡我們已經簡單的分析程式碼,修改一些抽象語法樹上的內容來達到我們的目的,但是還是有很多中情況還沒考慮進去,而babel現階段不僅僅代表著去轉換es6程式碼之類的功能,實際上我們自己可以寫出很多有意思的外掛,歡迎來了解babel,按照自己的想法寫一些外掛或者去貢獻一些程式碼,相信在這個過程中你收穫的絕對比你想象中的要更多!

相關文章