babel是一個轉碼器,目前開發react、vue專案都要使用到它。它可以把es6+的語法轉換為es5,也可以轉換JSX等語法等,實際上他能通過自定義外掛的方式完成任意轉換。
我們在專案中都是通過配置外掛和預設(多個外掛的集合)來轉換特定程式碼,例如env、stage-0等。那麼這些庫是如何實現的呢,下面就通過一個小例子探究一下--把es6的class
轉換為es5。
文章結構:
webpack環境配置
大家應該都配置過babel-core這個loader,實際上它的作用只是提供babel的核心Api,我們的程式碼轉換其實都是通過外掛來實現的。
接下來我們不用第三方的外掛,自己實現一個es6類轉換外掛。先執行以下幾步初始化一個專案:
- npm install webpack webpack-cli babel-core -D
- 新建一個webpack.config.js
- 配置webpack.config.js
如果我們的外掛名字想叫transform-class,需要在webpack配置中做如下配置:
接下來我們在node_modules中新建一個babel-plugin-transform-class的資料夾來寫外掛的邏輯(如果是真實專案,你需要編寫這個外掛併發布到npm倉庫),如下圖:
紅色區域是我新建的資料夾,它上面是一個標準的外掛的專案結構,為了方便我的外掛只寫了核心的index.js檔案。
如何編寫bable外掛
babel外掛其實是通過AST(抽象語法樹)實現的。
babel幫助我們把js程式碼轉換為AST,然後允許我們修改,最後再把它轉換成js程式碼。
那麼就涉及到兩個問題:js程式碼和AST之間的對映關係是什麼?如何替換或者新增AST?
好,先介紹一個工具:astexplorer.net:
這個工具可以把一段程式碼轉換為AST:
如圖,我們寫了一個es6的類,然後網頁的右邊幫我們生成了一個AST,其實就是把每一行程式碼變成了一個物件,這樣我們就實現了一個對映。再介紹一個文件:babel-types:
這是建立AST節點的Api文件。
比如,我們想建立一個類,先到astexplorer.net中轉換,發現類對應的AST型別是ClassDeclaration
。好,我們去文件中搜尋,發現呼叫下面的api就可以建立這樣一個節點:
下面我們開始真正編寫一個外掛,分為以下幾步:
- 在index.js中export一個函式
- 函式中返回一個物件,物件有一個visitor引數(必須叫visitor)
- 通過astexplorer.net查詢出
class
對應的AST節點為ClassDeclaration
- 在vistor中設定一個捕獲函式
ClassDeclaration
,意思是我要捕獲js程式碼中所有ClassDeclaration
節點 - 編寫邏輯程式碼,完成轉換
上面的步驟對應成程式碼:
module.exports = function ({ types: t }) {
return {
visitor: {
ClassDeclaration(path) {
//在這裡完成轉換
}
}
};
}
複製程式碼
程式碼中有兩個引數,第一個{types:t}
東西是從引數中解構出變數t,它其實就是babel-types文件中的t(下圖紅框),我們就是用這個t
建立節點:
第二個引數path
,它是捕獲到的節點對應的資訊,我們可以通過path.node
獲得這個節點的AST,在這個基礎上進行修改就能完成了我們的目標。
如何把es6的class轉換為es5的類
上面都是預備工作,真正的邏輯從現在才開始,我們先考慮兩個問題:
- 我們要做如下轉換,首先把es6的類,轉換為es5的類寫法(也就是普通函式),我們觀察到,很多程式碼是可以複用的,包括函式名字、函式內部的程式碼塊等。
- 如果不定義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有一點幫助。