babel是一個轉碼器,目前開發react、vue項目都要使用到它。它能夠把es6+的語法轉換爲es5,也能夠轉換JSX等語法等,實際上他能經過自定義插件的方式完成任意轉換。
咱們在項目中都是經過配置插件和預設(多個插件的集合)來轉換特定代碼,例如env、stage-0等。那麼這些庫是如何實現的呢,下面就經過一個小例子探究一下--把es6的class
轉換爲es5。
vue
你們應該都配置過babel-core這個loader,實際上它的做用只是提供babel的核心Api,咱們的代碼轉換其實都是經過插件來實現的。
接下來咱們不用第三方的插件,本身實現一個es6類轉換插件。先執行如下幾步初始化一個項目:
node
若是咱們的插件名字想叫transform-class,須要在webpack配置中作以下配置:react
接下來咱們在node_modules中新建一個babel-plugin-transform-class的文件夾來寫插件的邏輯(若是是真實項目,你須要編寫這個插件併發布到npm倉庫),以下圖:webpack
紅色區域是我新建的文件夾,它上面是一個標準的插件的項目結構,爲了方便個人插件只寫了核心的index.js文件。git
babel插件實際上是經過AST(抽象語法樹)實現的。
babel幫助咱們把js代碼轉換爲AST,而後容許咱們修改,最後再把它轉換成js代碼。
那麼就涉及到兩個問題:js代碼和AST之間的映射關係是什麼?如何替換或者新增AST?
es6
這個工具能夠把一段代碼轉換爲AST: github
如圖,咱們寫了一個es6的類,而後網頁的右邊幫咱們生成了一個AST,其實就是把每一行代碼變成了一個對象,這樣咱們就實現了一個映射。 這是建立AST節點的Api文檔。
好比,咱們想建立一個類,先到astexplorer.net中轉換,發現類對應的AST類型是ClassDeclaration
。好,咱們去文檔中搜索,發現調用下面的api就能夠建立這樣一個節點: web
下面咱們開始真正編寫一個插件,分爲如下幾步:npm
class
對應的AST節點爲ClassDeclaration
ClassDeclaration
,意思是我要捕獲js代碼中全部ClassDeclaration
節點上面的步驟對應成代碼:api
module.exports = function ({ types: t }) {
return {
visitor: {
ClassDeclaration(path) {
//在這裏完成轉換
}
}
};
}
複製代碼
代碼中有兩個參數,第一個{types:t}
東西是從參數中解構出變量t,它其實就是babel-types文檔中的t(下圖紅框),咱們就是用這個t
建立節點:
第二個參數path
,它是捕獲到的節點對應的信息,咱們能夠經過path.node
得到這個節點的AST,在這個基礎上進行修改就能完成了咱們的目標。
constructor
方法,JavaScript引擎會自動爲它添加一個空的constructor()
方法,這須要咱們作兼容處理。constructor
節點(上文提到,class中有可能沒有定義constructor)constructor
,若是是,經過老數據建立一個普通函數節點,並更新默認constructor
節點constructor
的節點,經過老數據建立prototype
類型的函數,並放到es5Fns
中constructor
節點也放到es5Fns
中replaceWithMultiple
這個API更新ASTmodule.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有一點幫助。