天天看點

探索babel和babel插件是怎麼工作的

你有可能會聽到過這個詞 webpack工程師 ,這個看似像是一個專業很強的職位其實很多時候是一些前端對現在前端工作方式對一些吐槽,對于一個之前沒有接觸過<code>webpack</code>,<code>nodejs</code>,<code>babel</code> 之類的工具的人來說,看到大量的配置檔案後很多人都會看懵

探索babel和babel插件是怎麼工作的

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

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

這裡我們去探索一下<code>babel</code>的原理

Babel · The compiler for writing next generation JavaScript

探索babel和babel插件是怎麼工作的

<code>redux</code> 的作者曾說過這樣一句話,可以換一種了解為

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

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

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

這種情況靠 <code>Polyfill</code>, 因為一些浏覽器根本就不識别這些代碼,這時候就需要把這些代碼轉換成浏覽器識别的代碼。<code>babel</code>就是做這個事情的。

探索babel和babel插件是怎麼工作的

為了轉換我們的代碼,<code>babel</code>做了三件事

<code>Parser</code> 解析我們的代碼轉換為<code>AST</code>。

<code>Transformer</code> 利用我們配置好的<code>plugins/presets</code>把<code>Parser</code>生成的<code>AST</code>轉變為新的<code>AST</code>。

<code>Generator</code> 把轉換後的<code>AST</code>生成新的代碼

從圖上看 <code>Transformer</code> 占了很大一塊比重,這個轉換過程就是<code>babel</code>中最複雜的部分,我們平時配置的<code>plugins/presets</code>就是在這個子產品起作用。

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

解析步驟接收代碼并輸出 <code>AST</code>,這其中又包含兩個階段詞法分析和文法分析。詞法分析階段把字元串形式的代碼轉換為 <code>令牌(tokens)</code> 流。文法分析階段會把一個令牌流轉換成 <code>AST</code> 的形式,友善後續操作。

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

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

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

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

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

探索babel和babel插件是怎麼工作的

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

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

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

探索babel和babel插件是怎麼工作的

<code>babel</code>拿到抽象文法樹後會使用<code>babel-traverse</code>進行遞歸的樹狀周遊,對于每一個節點都會向下周遊到盡頭,然後向上周遊退出分支去尋找下一個分支。這樣確定我們能找到任何一個節點,也就是能通路到我們代碼的任何一個部分。可是我們要怎麼去完成修改操作呢,<code>babel</code>給我們提供了下面這兩個概念。

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

這樣在周遊進入到對應到節點時候,<code>babel</code>就會去執行對應的<code>enter</code>函數,向上周遊退出對應節點時候,<code>babel</code>就會去執行對應的<code>exit</code>函數,接着上面的代碼我們可以做一個測試

我們執行對應代碼可以看到上面<code>enter</code>和<code>exit</code>函數分别執行了四次

從上面簡單的代碼上也可以看到<code>a,b,c,d</code>四個變量,它們應該屬于同一級别的節點樹上,是以周遊時候會分别進入對應節點然後退出再去下一個節點。

我們通過<code>visitor</code>可以在周遊到對應節點執行對應的函數,可是要修改對應節點的資訊,我們還需要拿到對應節點的資訊以及節點和所在的位置<code>(即和其他節點間的關系)</code>, <code>visitor</code>在周遊到對應節點執行對應函數時候會給我們傳入<code>path</code>參數,輔助我們完成上面這些操作。注意 <code>Path</code> 是表示兩個節點之間連接配接的對象,而不是目前節點,我們上面通路到了<code>Identifier</code>節點,它傳入的 <code>path</code>參數看起來是這樣的

從上面我們可以看到 <code>path</code> 表示兩個節點之間的連接配接,通過這個對象我們可以通路到節點、父節點以及進行一系列跟節點操作相關的方法。我們修改一下上面的 <code>visitor</code> 函數

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

<code>babel</code>為了友善我們開發,在每一個環節都有很多人性化的定義也提供了很多實用性的工具,比如之前我們在定義<code>visitor</code>時候分别定義了<code>enter</code>,<code>exit</code>函數,可很多時候我們其實隻用到了一次在<code>enter</code>的時候做一些處理就行了。是以我們如果我們直接定義節點的<code>key</code>為函數,就相當于定義了<code>enter</code>函數

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

假如我們有如下代碼

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

既然是<code>console.log</code>沒有寫全,我們就改變這個<code>log</code>函數調用的地方,把每一個<code>log</code>替換成<code>console.log</code>,我們看一下<code>log(*)</code>屬于函數執行語句,相對應的節點就是<code>CallExpression</code>,我們看下它的結構

執行後我們可以看到結果

我們已經知道每一個子產品都是一個對于的<code>AST</code>,而<code>AST</code>根節點是 <code>Program</code> 節點,下面的語句都是<code>body</code>上面的子節點,我們隻要在<code>body</code>頭聲明一下<code>log</code>變量,把它定義為<code>console.log</code>,後面這樣使用就也正常了。

這裡簡單的修改下visitor

執行後生成的代碼為

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

繼續閱讀