剖析Babel——Babel總覽

發表於2017-04-06

名詞解釋

AST:Abstract Syntax Tree, 抽象語法樹

DI: Dependency Injection, 依賴注入

===============================================================

Babel的解析引擎

Babel使用的引擎是babylon,babylon並非由babel團隊自己開發的,而是fork的acorn專案,acorn的專案本人在很早之前在興趣部落1.0在構建中使用,為了是做一些程式碼的轉換,是很不錯的一款引擎,不過acorn引擎只提供基本的解析ast的能力,遍歷還需要配套的acorn-travesal, 替換節點需要使用acorn-,而這些開發,在Babel的外掛體系開發下,變得一體化了

Babel的工作過程

Babel會將原始碼轉換AST之後,通過便利AST樹,對樹做一些修改,然後再將AST轉成code,即成原始碼。

上面提到Babel是fork acon專案,我們先來看一個來自興趣部落專案的,簡單的ACON示例

一個簡單的ACON轉換示例

解決的問題

轉換成

熟悉angular的同學都能看到這段程式碼做的是對DI的自動提取功能,使用ACON手動擼程式碼

具體的流程如下

可以從上面的過程看到acorn的特點

1.acorn做為一款優秀的原始碼解析器

2.acorn並不提供對AST樹的修改能力

3.acorn並不提供AST樹的還原能力

4.修改原始碼仍然靠原始碼修改字串的方式

Babel正是擴充套件了acorn的能力,使得轉換變得更一體化

Babel的前序工作——Babylon、babel-types:code轉換為AST

Babel轉AST樹的過程涉及到語法的問題,轉AST樹一定有對就的語法,如果在解析過程中,出現了不符合Babel語法的程式碼,就會報錯,Babel轉AST的解析過程在Babylon中完成

解析成AST樹使用babylon.parse方法

結果如下

AST如下

關於AST樹的詳細定義Babel有文件

https://github.com/babel/babylon/blob/master/ast/spec.md

關於AST樹的定義

ast中的節點都是繼承自Node節點,Node節點有type和loc兩個屬性,分別代表型別和位置,

其中位置定義如下

 位置節點又是由source(原始碼string), 開始位置,結束位置組成,start,end又是Position型別

節點又包含行號和列號

再看Program的定義

Program是繼承自Node節點,型別是Program, sourceType有兩種,一種是script,一種是module,程式體是一個宣告體Statement或者模組宣告體ModuleDeclaration節點陣列

Babylon支援的語法

Babel或者說Babylon支援的語法現階段是不可以第三方擴充套件的,也就是說我們不可以使用babylon做一些奇奇怪的語法,換句話說

不要希望通過babel的外掛體系來轉換自己定義的語法規則

那麼babylon支援的語法有哪些呢,除了常規的js語法之外,babel暫時只支援如下的語法

Plugins

  • estree
  • jsx
  • flow
  • doExpressions
  • objectRestSpread
  • decorators (Based on an outdated version of the Decorators proposal. Will be removed in a future version of Babylon)
  • classProperties
  • exportExtensions
  • asyncGenerators
  • functionBind
  • functionSent
  • dynamicImport

如果要真要自定義語法,可以在babylon的plugins目錄下自定義語法

https://github.com/babel/babylon/tree/master/src/plugins

Babel-types,擴充套件的AST樹

上面提到的babel的AST文件中,並沒有提到JSX的語法樹,那麼JSX的語法樹在哪裡定義呢,同樣jsx的AST樹也應該在這個文件中指名,然而babel團隊還沒精力準備出來

實際上,babel-types有擴充套件AST樹,babel-types的definitions就是天然的文件,具體的原始碼定義在這裡

舉例一個AST節點如查是JSXElement,那麼它的定義可以在jsx.js中找到

 JSXElement的builder欄位指明要構造一個這樣的節點需要4個引數,這四個引數分別對應在fields欄位中,四個引數的定義如下

openingElement: 必須是一個JSXOpeningElement節點

closingElement: 必須是一個JSXClosingElement節點

children: 必須是一個陣列,陣列元素必須是JSXText、JSXExpressionContainer、JSXSpreadChild中的一種型別

selfClosing: 未指明驗證

使用 babel-types.[TYPE]方法就可以構造這樣的一個AST節點

構造了一個jsxElement型別的節點,這在Babel外掛開發中是很重要的

同樣驗證是否一個JSXElement節點,也可以使用babel-types.isTYPE方法

比如

所以用JSXElement語法定義可以直接看該檔案,簡單做個梳理如下

其中,斜體代表非終結符,粗體為終結符

Babel的中序工作——Babel-traverse、遍歷AST樹,外掛體系

  • 遍歷的方法
    一旦按照AST中的定義,解析成一顆AST樹之後,接下來的工作就是遍歷樹,並且在遍歷的過程中進行轉換

Babel負責便利工作的是Babel-traverse包,使用方法

遍歷結點讓我們可以獲取到我們想要操作的結點的可能,在遍歷一個節點時,存在enter和exit兩個時刻,一個是進入結點時,這個時候節點的子節點還沒觸達,遍歷子節點完成的時刻,會離開該節點,所以會有exit方法觸發

訪問節點,可以使用的引數是path引數,path這個引數並不直接等同於節點,path的屬性有幾個重要的組成,如下

舉個例子,如下的程式碼會將所有function變成另外的function

結果如下

注意這裡我們使用babel-types來判別node的型別,使用path的replaceWithSourceString方法來替換節點

但這裡在babel的文件中也有提示,儘量少用replaceWithSourceString方法,該方法一定會呼叫babylon.parse解析程式碼,在遍歷中解析程式碼,不如將解析程式碼放到遍歷外面去做

其實上面的過程只是定義瞭如何遍歷節點的時候轉換節點

babel將上面的便利操作對外開放出去了,這就構成了babel的外掛體系

babel的外掛體系——結點的轉換定義

babel的外掛就是定義如何轉換當前結點,所以從這裡可以看出babel的外掛能做的事情,只能轉換ast樹,而不能在作用在前序階段(語法分析)

這裡不得不提下babel的外掛體系是怎麼樣的,babel的外掛分為兩部分

  • babel-preset-xxx
  • babel-plugin-xxx

preset: 預設, preset和plugin其實是一個東西,preset定義了一堆plugin list

這裡值得一提的是,preset的順序是倒著的,plugin的順序是正的,也就是說

preset: [‘es2015’, ‘react’], 其實是先使用react外掛再用es2015

plugin: [‘transform-react’, ‘transfrom-async-function’] 的順序是正的遍歷節點的時候先用transform-react再用transfrom-async-function

babel外掛編寫

如果是自定義外掛,還在開發階段,要先在babel的配置檔案指明babel外掛的路徑

babel的自定義外掛寫法是多樣,上面只是一個例子,可以傳入option,具體可以參考babel的配置文件

上面的程式碼寫成babel的外掛如下

Babel的外掛包return一個function, 包含babel的引數,function執行後返回一個包含visitor的物件,物件的屬性是遍歷節點匹配到該型別的處理方法,該方法依然包含enter和exit方法

一些AST樹的建立方法

在寫外掛的過程中,經常要建立一些AST樹,常用的方法如下

  • 使用babel-types定義的建立方法建立
    比如建立一個var a = 1;

如果使用這樣建立一個ast節點,肯定要累死了

  • 使用replaceWithSourceString方法建立替換
  • 使用template方法來建立AST結點
    template方法其實也是babel體系中的一部分,它允許使用一些模板來建立ast節點

比如上面的var a = 1可以使用

當然也可以簡單寫

接下來就可以用path的增、刪、改操作進行轉換了

Babel的後序工作——Babel-generator、AST樹轉換成原始碼

Babel-generator的工作就是將一顆ast樹轉回來,具體操作如下

至此,程式碼轉換就算完成了

Babel的外圍工作——Babel-register,動態編譯

通常我們都是使用webpack編譯後程式碼再執行程式碼的,使用Babel-register允許我們不提前編譯程式碼就可以執行程式碼,這在node端是非常便利的

在node端,babel-regiser的核心實現是下面這兩個程式碼

通過定義require.extensions方法,可以覆蓋require方法,這樣呼叫require的時候,就可以走babel的編譯,然後使用m._compile方法執行程式碼

但這個方法在node是不穩定的方法

結語

最後,就像babylon官網感覺acorn一樣,babel為前端界做了一件awesome的工作,有了babel,不僅僅可以讓我們的新技術的普及提前幾年,我們可以通過寫外掛做更多的事情,比如做自定義規則的驗證,做node的直出node端的適配工作等等。

參考資料

babel官網: https://babeljs.io

babel-github: https://github.com/babel

babylon: https://github.com/babel/babylon

acorn: https://github.com/marijnh/acorn

babel-ast文件: https://github.com/babel/babylon/blob/master/ast/spec.md

babel外掛cookbook: https://github.com/thejameskyle/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md

babel-packages: https://github.com/babel/babel/tree/7.0/packages

babel-types-definitions: https://github.com/babel/babel/tree/7.0/packages/babel-types/src/definitions

相關文章