你有可能會聽到過這個詞 webpack工程師 ,這個看似像是一個專業很強的職位其實不少時候是一些前端對如今前端工做方式對一些吐槽,對於一個以前沒有接觸過webpack
,nodejs
,babel
之類的工具的人來講,看到大量的配置文件後不少人都會看懵javascript
不少人就乾脆無論這些東西,直接上手寫業務代碼,把這些構建工具就至關於黑科技
,咱們把全部的文件都通過這些工具最終生成一個或者幾個打包後的文件,其中關於優化和代碼轉換問題其實一大部分都是在這些配置裏面的。若是咱們不去了解其中的一部分原理,後面遇到不少問題(如打包後文件體積過大
)時候都是一籌莫展,並且萬一哪天構建工具出現問題時候可能連工做都開展不下去了。前端
既然咱們平常都要用到,最好的方式就是去研究一下這些工具的原理的做用,讓這些工具成爲咱們手中的利器,而不是工做上的絆腳石,並且這些工具的設計者都是頂級的工程師,當你敲開壁壘探究內部祕密時候,我相信你會感覺到其中的編程之美。java
這裏咱們去探索一下babel
的原理node
Babel · The compiler for writing next generation JavaScriptwebpack
你在npm
上能夠看到這樣一個包名字是6to5, 光看名字可能會讓人感受到很詫異,名字看起來可能有點奇怪,其實babel
在開始的時候名字就是這個。簡單粗暴es6 -> es5
,一會兒就看懂了babel
是用來幹啥的,可是很明顯這不是一個好名字,這個名字會讓人感受到es6
普及以後這個庫就沒用了,爲了保持活力這個庫可能要不停的修更名字。下面是babel
做者一次分享中假設若是按這個命名法則可能出現的名稱git
很明顯發生這種狀況是很不合理的,團隊內部通過大量討論後,最終選擇了babel
,這與電影銀河系漫遊指南中的Babel fish相應,也有關係到聖經中的一個故事Tower of Babel。(ps.優秀的人老是也頗有情懷。)
es6
redux
的做者曾說過這樣一句話,能夠換一種理解爲github
babel : AST :: jQuery : DOM
babel
對於 AST
就至關於 jQuery
對於 DOM
, 就是說babel
給予了咱們便捷查詢和修改 AST
的能力。(AST -> Abstract Syntax Tree) 抽象語法樹 後面會講到。
web
咱們以前作一些兼容都會都會接觸一些 Polyfill
的概念,好比若是某個版本的瀏覽器不支持 Array.prototype.find
方法,可是咱們的代碼中有用到Array
的find
函數,爲了支持這些代碼,咱們會人爲的加一些兼容代碼shell
if (!Array.prototype.find) { Object.defineProperty(Array.prototype, 'find', { // 實現代碼 ... }); }
對於這種狀況作兼容也很好實現,引入一個 Polyfill
文件就能夠了,可是有一些狀況咱們使用到了一些新語法,或者一些其餘寫法
// 箭頭函數 var a = () => {} // jsx var Component = () => <div />
這種狀況靠 Polyfill
, 由於一些瀏覽器根本就不識別這些代碼,這時候就須要把這些代碼轉換成瀏覽器識別的代碼。babel
就是作這個事情的。
爲了轉換咱們的代碼,babel
作了三件事
Parser
解析咱們的代碼轉換爲AST
。Transformer
利用咱們配置好的plugins/presets
把Parser
生成的AST
轉變爲新的AST
。Generator
把轉換後的AST
生成新的代碼從圖上看 Transformer
佔了很大一塊比重,這個轉換過程就是babel
中最複雜的部分,咱們平時配置的plugins/presets
就是在這個模塊起做用。
能夠看到要想搞懂babel
, 就是去了解上面三個步驟都是在幹什麼,咱們先把比較容易看懂的地方開始瞭解一下。
解析步驟接收代碼並輸出 AST
,這其中又包含兩個階段詞法分析和語法分析。詞法分析階段把字符串形式的代碼轉換爲 令牌(tokens)
流。語法分析階段會把一個令牌流轉換成 AST
的形式,方便後續操做。
代碼生成步驟把最終(通過一系列轉換以後)的 AST 轉換成字符串形式的代碼,同時還會建立源碼映射(source maps)。代碼生成其實很簡單:深度優先遍歷整個 AST,而後構建能夠表示轉換後代碼的字符串。
看起來babel
的主要工做都集中在把解析生成的AST
通過plugins/presets
而後去生成新的AST
這上面了。
咱們一直在提到AST
它到底是什麼呢,既然它的名字叫作抽象語法樹
,咱們能夠想象一下若是把咱們的程序用樹狀表示會是什麼樣呢。
var a = 1 + 1 var b = 2 + 2
咱們想象一下要表示上述代碼應該是什麼樣子,首先必須有東西能夠表示這些具體的聲明
,變量
,常量
的具體信息,好比(這棵樹上確定有二個變量,變量名是a和b,確定有兩個運算語句,操做符是 + )
,有了這些信息還不夠,咱們必須創建起它們之間的關係,好比一個聲明語句,聲明類型是 var, 左側是變量, 右側是表達式
。有了這些信息咱們就能夠還原這個程序,這也是把代碼解析成AST
時候所作的事情,對應上面咱們說的詞法分析
和 語法分析
。
在AST
中咱們用node
(節點)來表示各個代碼片斷,好比咱們上面程序總體就是一個節點Program
節點(全部的 AST 根節點都是 Program 節點),由於它下面有兩條語句因此它的 body
屬性上就兩個聲明節點VariableDeclaration
。因此上面程序的AST
就相似這樣
能夠看到在節點上用各個的屬性去表示各類信息以及程序之間的關係,那這些節點每個叫什麼名字,都用哪些屬性名呢?咱們能夠在說明文檔上找到這些說明。
看這個文檔時候咱們能夠看到說明大可能是相似這種
interface Node { type: string; loc: SourceLocation | null; }
這裏提到interface
這個咱們在其餘語言中是比較常見的,好比Node
規定了type
和loc
屬性,若是其餘節點繼承自Node
,那麼它也會實現type
和loc
屬性就是說繼承自Node
的節點也會有這些屬性,基本全部節點都繼承自Node
,因此咱們基本能夠看到loc
這個屬性loc
表示個一些位置信息。
咱們程序不少地方都會被拆分紅一個個的節點,節點裏面也會套着其餘的節點,咱們在文檔中能夠看到AST
結構的各個 Node
節點都很細微,好比咱們聲明函數,函數就是一個節點FunctionDeclaration
,函數名和形參那麼參數都是一個變量節點Identifier
。生成的節點每每都很複雜,咱們能夠藉助astexplorer來幫助咱們分析AST
結構。
有了上面這些概念咱們已經能夠大概瞭解AST
的概念,以及各個模塊表明的含義,假設咱們有這樣一個程序,咱們用圖形簡易的分析下它的結構
function square (n) { return n * n }
通過一番努力咱們終於瞭解了AST
以及其中內容的含義,可是這一部分基本不須要咱們作什麼,babel
會藉助Babylon幫咱們生成咱們須要的AST
結構。咱們更多要去作的是去修改和改變Babylon
生成的這個抽象語法樹。
babel
拿到抽象語法樹後會使用babel-traverse
進行遞歸的樹狀遍歷,對於每個節點都會向下遍歷到盡頭,而後向上遍歷退出分支去尋找下一個分支。這樣確保咱們能找到任何一個節點,也就是能訪問到咱們代碼的任何一個部分。但是咱們要怎麼去完成修改操做呢,babel
給咱們提供了下面這兩個概念。
咱們已經知道babel
會遍歷節點組成的抽象語法樹,每個節點都會有本身對應的type
,好比變量節點Identifier
等。咱們須要給babel
提供一個visitor
對象,在這個對象上面咱們以這些節點的type
作爲key
,已一個函數做爲值,相似以下,
const visitor = { Identifier: { enter() { console.log('traverse enter a Identifier node!') }, exit() { console.log('traverse exit a Identifier node!') } } }
這樣在遍歷進入到對應到節點時候,babel
就會去執行對應的enter
函數,向上遍歷退出對應節點時候,babel
就會去執行對應的exit
函數,接着上面的代碼咱們能夠作一個測試
const babel = require('babel-core') const code = `var a = b + c + d` // 若是plugins是個函數則返回的對象要有visitor屬性,若是是個對象則直接定義visitor屬性 const MyVisitor = { visitor } babel.transform(code, { plugins: [MyVisitor] })
咱們執行對應代碼能夠看到上面enter
和exit
函數分別執行了四次
traverse enter a Identifier node! traverse exit a Identifier node! ... x4
從上面簡單的代碼上也能夠看到a,b,c,d
四個變量,它們應該屬於同一級別的節點樹上,因此遍歷時候會分別進入對應節點而後退出再去下一個節點。
咱們經過visitor
能夠在遍歷到對應節點執行對應的函數,但是要修改對應節點的信息,咱們還須要拿到對應節點的信息以及節點和所在的位置(即和其餘節點間的關係)
, visitor
在遍歷到對應節點執行對應函數時候會給咱們傳入path
參數,輔助咱們完成上面這些操做。注意 Path
是表示兩個節點之間鏈接的對象,而不是當前節點,咱們上面訪問到了Identifier
節點,它傳入的 path
參數看起來是這樣的
{ "parent": { "type": "VariableDeclarator", "id": {...}, .... }, "node": { "type": "Identifier", "name": "..." } }
從上面咱們能夠看到 path
表示兩個節點之間的鏈接,經過這個對象咱們能夠訪問到節點、父節點以及進行一系列跟節點操做相關的方法。咱們修改一下上面的 visitor
函數
const visitor = { Identifier: { enter(path) { console.log('traverse enter a Identifier node the name is ' + path.node.name) }, exit(path) { console.log('traverse exit a Identifier node the name is ' + path.node.name) } } }
在執行一下上面的代碼就能夠看到name
打印出來的依次是a
,b
,c
,d
。這樣咱們就有能夠修改操做咱們須要改變的節點了。另外path
對象上還包含添加、更新、移動和刪除節點有關的其餘不少方法,咱們能夠經過文檔去了解。
babel
爲了方便咱們開發,在每個環節都有不少人性化的定義也提供了不少實用性的工具,好比以前咱們在定義visitor
時候分別定義了enter
,exit
函數,可不少時候咱們其實只用到了一次在enter
的時候作一些處理就好了。因此咱們若是咱們直接定義節點的key
爲函數,就至關於定義了enter
函數
const visitor = { Identifier(){ // dosmting } } // 等同於 ↓ ↓ ↓ ↓ ↓ ↓ const visitor = { Identifier: { enter() { // dosmting } } }
上面咱們還提到了plugins是函數的狀況,其實咱們寫的差距通常都是一個函數,這個入口函數上babel
也會穿入一個babel-types
,這是一個用於AST
節點的 Lodash
式工具庫(相似lodash
對於js
的幫助), 它包含了構造、驗證以及變換 AST 節點的方法。 該工具庫包含考慮周到的工具方法,對編寫處理AST邏輯很是有用。
假如咱們有以下代碼
const a = 3 * 103.5 * 0.8 log(a) const b = a + 105 - 12 log(b)
咱們發現這裏把console.log
簡寫成了log
,爲了讓這些代碼能夠執行,咱們如今用babel
裝置去轉換一下這些代碼。
既然是console.log
沒有寫全,咱們就改變這個log
函數調用的地方,把每個log
替換成console.log
,咱們看一下log(*)
屬於函數執行語句,相對應的節點就是CallExpression
,咱們看下它的結構
interface CallExpression <: Expression { type: "CallExpression"; callee: Expression | Super | Import; arguments: [ Expression | SpreadElement ]; optional: boolean | null; }
callee
是咱們函數執行的名稱,arguments
就是咱們穿入的參數,參數咱們不須要改變,只須要把函數名稱改變就行了,以前的callee
是一個變量,咱們如今要把它變成一個表達式(取對象屬性值的表達式)
,咱們看一下手冊能夠看到是一個MemberExpression
類型的值,這裏也能夠藉助以前提到的網站astexplorer來幫助咱們分析。有了這些信息咱們就能夠去實現咱們的目的了,咱們這裏手動引入一下babel-types
輔助咱們建立新的節點
const babel = require('babel-core') const t = require('babel-types') const code = ` const a = 3 * 103.5 * 0.8 log(a) const b = a + 105 - 12 log(b) ` const visitor = { CallExpression(path) { // 這裏判斷一下若是不是log的函數執行語句則不處理 if (path.node.callee.name !== 'log') return // t.CallExpression 和 t.MemberExpression分別表明生成對於type的節點,path.replaceWith表示要去替換節點,這裏咱們只改變CallExpression第一個參數的值,第二個參數則用它本身原來的內容,即原本有的參數 path.replaceWith(t.CallExpression( t.MemberExpression(t.identifier('console'), t.identifier('log')), path.node.arguments )) } } const result = babel.transform(code, { plugins: [{ visitor: visitor }] }) console.log(result.code)
執行後咱們能夠看到結果
const a = 3 * 103.5 * 0.8; console.log(a); const b = a + 105 - 12; console.log(b);
咱們已經知道每個模塊都是一個對於的AST
,而AST
根節點是 Program
節點,下面的語句都是body
上面的子節點,咱們只要在body
頭聲明一下log
變量,把它定義爲console.log
,後面這樣使用就也正常了。
這裏簡單的修改下visitor
const visitor = { Program(path) { path.node.body.unshift( t.VariableDeclaration( 'var', [t.VariableDeclarator( t.Identifier('log'), t.MemberExpression(t.identifier('console'), t.identifier('log')) )] ) ) } }
執行後生成的代碼爲
var log = console.log; const a = 3 * 103.5 * 0.8; log(a); const b = a + 105 - 12; log(b);
到這裏咱們已經簡單的分析代碼,修改一些抽象語法樹上的內容來達到咱們的目的,可是仍是有不少中狀況還沒考慮進去,而babel
現階段不只僅表明着去轉換es6
代碼之類的功能,實際上咱們本身能夠寫出不少有意思的插件,歡迎來了解babel
,按照本身的想法寫一些插件或者去貢獻一些代碼,相信在這個過程當中你收穫的絕對比你想象中的要更多!
本文首發與 我的博客