babel原理及外掛開發

新垣結衣發表於2019-03-03

摘要

如今的前端界已經離不開ES6,然而老舊瀏覽器並不支援,專案中特別是國內公司又需要相容低版本的老舊瀏覽器,多虧了babel這個神奇的工具,可以讓我們的ES6程式碼執行在舊瀏覽器中。

大部分前端開發人員只是配置一下babel,根據需要裝個外掛之類,我想肯定少有人去研究babel轉換ES6程式碼的原理及外掛原理,於是在某個日子裡由於專案的需要去研究了一下babel的原理。

需要說明的是,本文不涉及babel的用法,不論是看官網文件還是其他這類文章都太多,本文會結合自己曾經寫的一個babel外掛來分析babel的原理。

分析

babel實際上類似一般的的語言編譯器,作用就是輸入輸入程式碼,實際上跟很多人理解的不太一樣,babel並不是只能用於ES6編譯成ES5,只要你願意,你完全可以把ES5編譯成ES6,或者使用自己創造的某種語法(例如JSX,以及本文結合的babel外掛就屬於這類),你需要做的只是編寫對應的外掛。

babel轉換程式碼的過程主要為三步:

解析

使用babylon這個解析器,它會根據輸入的javascript程式碼字串根據ESTree規範生成AST(抽象語法樹)。

轉換

根據一定的規則轉換、修改AST。

生成

使用babel-generator將修改後的AST轉換成普通程式碼。

這就是babel工作的整個過程,就是純粹的字串輸入輸出而已,而babel外掛或者預置的stage-0,1,2,3,jsx等,都是第二步轉換的“規則”。

什麼是AST?

在電腦科學中,抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是原始碼語法結構的一種抽象表示。它以樹狀的形式表現程式語言的語法結構,樹上的每個節點都表示原始碼中的一種結構。

如果要理解babel的原理,理解AST是必不可少的,儘管前端同學可能平時對於程式碼編譯這類接觸不多,但也應該瞭解AST。

我們都知道javascript程式碼是由一系列字元組成的,我們看一眼字元就知道它是幹什麼的,例如變數宣告、賦值、括號、函式呼叫等等。但是計算機並沒有眼睛可以看到,它需要某種機制去理解程式碼字串,基於此考慮為了讓人和計算機都能夠理解程式碼,就有了AST這麼個東西,它是原始碼的一種對映,在某種規則中二者可以相互轉化,語言引擎根據AST就能知道程式碼的作用是什麼。

以下一句簡單的變數宣告

var a = 1;
複製程式碼

當這句宣告生成AST後,可以得到以下的樹形結構

Imgur

body中就是主體程式碼的資訊,可以看到VariableDeclaration,即變數宣告,在declarations陣列中就是宣告的詳情。

具體可以在astexplorer.net/中檢視,你可以在左側輸入任何程式碼,右側會對應顯示生成的AST。

編寫外掛

簡介

本文編寫的外掛為babel-plugin-webpack-async-module-name,用途是在webpack中為import()非同步模組命名。

具體轉換是轉換以下方法呼叫:

importName('./a.js', 'name-a');
複製程式碼

ES6是提供了一個import()方法用於動態匯入模組,然而這個方法只有一個路徑引數,沒有能夠為動態模組命名之類的引數,好在webpack社群提供了一種在webpack中的命名方式:

import( /*webpackChunkName: 'name-a'*/'./a.js');
複製程式碼

在使用的時候加入一行註釋,根據註釋中的webpackChunkName的值,結合webpack配置的output的chunkName為模組命名,具體可以檢視webpack文件。

然而這樣必須在每次呼叫的時候手動新增註釋及註釋中的名字,強迫症是無法忍受的,於是想了想發現babel可以實現一個自定義方法接收模組名生成帶註釋的import()方法,這個外掛作用就是生成這個帶註釋的import()的方法。

編寫

babel-plugin-xxx.js中匯出一個函式

module.exports = function(babel) {
  var t = babel.types
  return {
    visitor: {

    }
  }
}
複製程式碼

babel的外掛系統基於訪問者模式設計,我們編寫的這個函式就是為訪問者模式提供一個介面。

babel.types包含裡處理AST的一系列工具方法,具體可以檢視文件,實際編寫的時候,建議在astexplorer.net/中輸入編譯前後的程式碼,對比AST的區別,然後通過babel.types提供的方法修改AST即可。

編寫babel外掛首先需要知道要處理的哪種語法,具體到上面的這個外掛中,需要處理的是函式呼叫,那麼可以在visitor中新增CallExpression屬性,代表處理的是函式呼叫,以下是具體程式碼。

visitor: {
  CallExpression: function (path) {
    const {node} = path
    if (t.isIdentifier(node.callee, {name: 'importName'})) {
      const [module, name] = node.arguments
      if (name) {
        module.leadingComments = [{
          type: "CommentBlock",
          value: `webpackChunkName: '${name.value}'`
        }]
      }
      path.replaceWith(
        t.CallExpression(
          t.identifier('import'),
          [module]
        )
      )
    }
  }
}
複製程式碼

path是處理的AST節點的路徑,接著需要判斷具體呼叫的函式方法為importName,中間根據name加上註釋,然後使用path.replaceWith替換為新的CallExpression即可。

這樣經過babel外掛處理後,程式碼中的importName('./a.js', 'name-a')的AST就會被轉成正確的import()方法的AST,並加上註釋。

生成

這一步就是根據babel配置中presets和plugin選項中定義的規則產生的新的AST去生成正常的程式碼。

總結

babel就是一個編譯工具,根據輸入字串得到輸出,解析 -> 轉換 -> 生成就是大致原理,至於如何從正常程式碼解析成AST以及根據AST生成程式碼,我認為這是語言的編譯原理相關,babel的解析器只是其中的一種實現,而babel的強大之處在於提供的豐富AST轉換工具(好吧實際上是半路出家對於編譯原理不甚瞭解就不好意思獻醜了)。

最後再次提一下這個外掛babel-plugin-webpack-async-module-name,目前一定規模的前端應用採用程式碼非同步載入是必然趨勢,強迫症如果不想寫一堆require.ensure()...一堆東西或者想使用import()作為非同步模組匯入並簡單地自定義模組名,可以嘗試下這個外掛?。

本文來自babel原理及外掛開發

相關文章