入門babel--實現一個es6的class轉換器

      babel是一個轉碼器,目前開發react、vue項目都要使用到它。它能夠把es6+的語法轉換爲es5,也能夠轉換JSX等語法等,實際上他能經過自定義插件的方式完成任意轉換。
      咱們在項目中都是經過配置插件和預設(多個插件的集合)來轉換特定代碼,例如env、stage-0等。那麼這些庫是如何實現的呢,下面就經過一個小例子探究一下--把es6的class轉換爲es5。
vue

文章結構:

webpack環境配置

      你們應該都配置過babel-core這個loader,實際上它的做用只是提供babel的核心Api,咱們的代碼轉換其實都是經過插件來實現的。
      接下來咱們不用第三方的插件,本身實現一個es6類轉換插件。先執行如下幾步初始化一個項目:
node

  • npm install webpack webpack-cli babel-core -D
  • 新建一個webpack.config.js
  • 配置webpack.config.js

      若是咱們的插件名字想叫transform-class,須要在webpack配置中作以下配置:react

      接下來咱們在node_modules中新建一個babel-plugin-transform-class的文件夾來寫插件的邏輯(若是是真實項目,你須要編寫這個插件併發布到npm倉庫),以下圖:webpack

      紅色區域是我新建的文件夾,它上面是一個標準的插件的項目結構,爲了方便個人插件只寫了核心的index.js文件。git

如何編寫bable插件

      babel插件實際上是經過AST(抽象語法樹)實現的。
      babel幫助咱們把js代碼轉換爲AST,而後容許咱們修改,最後再把它轉換成js代碼。
      那麼就涉及到兩個問題:js代碼和AST之間的映射關係是什麼?如何替換或者新增AST?
es6

好,先介紹一個工具:astexplorer.net:

      這個工具能夠把一段代碼轉換爲AST: github

      如圖,咱們寫了一個es6的類,而後網頁的右邊幫咱們生成了一個AST,其實就是把每一行代碼變成了一個對象,這樣咱們就實現了一個映射。

再介紹一個文檔:babel-types:

      這是建立AST節點的Api文檔。
      好比,咱們想建立一個類,先到astexplorer.net中轉換,發現類對應的AST類型是ClassDeclaration。好,咱們去文檔中搜索,發現調用下面的api就能夠建立這樣一個節點: web

      同理,建立其餘節點也是同樣的道理。有了上面這兩個東西,咱們就能夠作任何轉換了。

      下面咱們開始真正編寫一個插件,分爲如下幾步:npm

  • 在index.js中export一個函數
  • 函數中返回一個對象,對象有一個visitor參數(必須叫visitor)
  • 經過astexplorer.net查詢出class對應的AST節點爲ClassDeclaration
  • 在vistor中設置一個捕獲函數ClassDeclaration,意思是我要捕獲js代碼中全部ClassDeclaration節點
  • 編寫邏輯代碼,完成轉換

      上面的步驟對應成代碼:api

module.exports = function ({ types: t }) {
    return {
        visitor: {
            ClassDeclaration(path) {
                //在這裏完成轉換
            }
        }
    };
}
複製代碼

      代碼中有兩個參數,第一個{types:t}東西是從參數中解構出變量t,它其實就是babel-types文檔中的t(下圖紅框),咱們就是用這個t建立節點:

      第二個參數path,它是捕獲到的節點對應的信息,咱們能夠經過path.node得到這個節點的AST,在這個基礎上進行修改就能完成了咱們的目標。

如何把es6的class轉換爲es5的類

上面都是預備工做,真正的邏輯從如今纔開始,咱們先考慮兩個問題:
  1. 咱們要作以下轉換,首先把es6的類,轉換爲es5的類寫法(也就是普通函數),咱們觀察到,不少代碼是能夠複用的,包括函數名字、函數內部的代碼塊等。

  1. 若是不定義class中的constructor方法,JavaScript引擎會自動爲它添加一個空的constructor()方法,這須要咱們作兼容處理。
接下來咱們開始寫代碼,思路是:
  • 拿到老的AST節點
  • 建立一個數組用來盛放新的AST節點(雖然原class只是一個節點,可是替換後它會被若干個函數節點取代)
  • 初始化默認的constructor節點(上文提到,class中有可能沒有定義constructor)
  • 循環老節點的AST對象(會循環出若干個函數節點)
  • 判斷節點的類型是否是constructor,若是是,經過老數據建立一個普通函數節點,並更新默認constructor節點
  • 處理其他不是constructor的節點,經過老數據建立prototype類型的函數,並放到es5Fns
  • 循環結束,把constructor節點也放到es5Fns
  • 判斷es5Fns的長度是否大於1,若是大於1使用replaceWithMultiple這個API更新AST
module.exports = function ({ types: t }) {
    return {
        visitor: {
            ClassDeclaration(path) {
                //拿到老的AST節點
                let node = path.node
                let className = node.id.name
                let classInner = node.body.body
                //建立一個數組用來成盛放新生成AST
                let es5Fns = []
                //初始化默認的constructor節點
                let newConstructorId = t.identifier(className)
                let constructorFn = t.functionDeclaration(newConstructorId, [t.identifier('')], t.blockStatement([]), false, false)
                //循環老節點的AST對象
                for (let i = 0; i < classInner.length; i++) {
                    let item = classInner[i]
                    //判斷函數的類型是否是constructor
                    if (item.kind == 'constructor') {
                        let constructorParams = item.params.length ? item.params[0].name : []
                        let newConstructorParams = t.identifier(constructorParams)
                        let constructorBody = classInner[i].body
                        constructorFn = t.functionDeclaration(newConstructorId, [newConstructorParams], constructorBody, false, false)
                    } 
                    //處理其他不是constructor的節點
                    else {
                        let protoTypeObj = t.memberExpression(t.identifier(className), t.identifier('prototype'), false)
                        let left = t.memberExpression(protoTypeObj, t.identifier(item.key.name), false)
                        //定義等號右邊
                        let prototypeParams = classInner[i].params.length ? classInner[i].params[i].name : []
                        let newPrototypeParams = t.identifier(prototypeParams)
                        let prototypeBody = classInner[i].body
                        let right = t.functionExpression(null, [newPrototypeParams], prototypeBody, false, false)
                        let protoTypeExpression = t.assignmentExpression("=", left, right)
                        es5Fns.push(protoTypeExpression)
                    }

                }
                //循環結束,把constructor節點也放到es5Fns中
                es5Fns.push(constructorFn)
                //判斷es5Fns的長度是否大於1
                if (es5Fns.length > 1) {
                    path.replaceWithMultiple(es5Fns)
                } else {
                    path.replaceWith(constructorFn)
                }
            }
        }
    };
}

複製代碼

優化繼承

      其實,類還涉及到繼承,思路也不復雜,就是判斷AST中有沒有superClass屬性,若是有的話,咱們須要多添加一行代碼Bird.prototype = Object.create(Parent),固然別忘了處理super關鍵字。

打包後代碼

      運行 npm start打包後,咱們看到打包後的文件裏 class語法已經成功轉換爲一個個的es5函數。

結尾

      如今一個類轉換器就寫完了,但願能對你們瞭解babel有一點幫助。

參考內容

github-babel插件開發指南
babel-types

相關文章
相關標籤/搜索