最近在一些項目編譯系統的工做中涉及到了不少關於babel插件的開發,關於babel大多數人的感覺多是既陌生又熟悉,可能大多數人對於babel的應用場景的認識就是在webpack中使用一個babel-loader,但當你真正瞭解他掌握它的時候,會發現他其實還有些更強的用法。。。前端
Babel 是一個編譯器(輸入源碼 => 輸出編譯後的代碼)。就像其餘編譯器同樣,編譯過程分爲三個階段:解析、轉換和打印輸出。(官網的解釋)。node
babel中有不少概念,好比:插件(plugin),預設(preset)和一些比較基礎的工具(例如@babel/parser,@babel/traverse等等)。關於他們的關係,能夠理解爲babel的plugin構建在基礎工具之上,而babel的preset是多個babel plugin的打包集合,例如咱們所熟悉的@babel/preset-env,@babel/preset-react。react
本篇文章不對babel官方的plugin,preset庫作過多闡述,畢竟這是一篇深刻教程。咱們要提的是一個更本質的問題:babel是如何轉譯代碼的?webpack
咱們大致上把這個轉譯代碼的過程分爲三步:git
這三步分別對應babel的三個基本工具,第一步對應@babel/parser,第二步對應@babel/traverse,第三步對應@babel/generator。下面就來詳述一下這三個過程。github
這一步是babel將code轉化爲ast。ast是Abstract syntax tree的縮寫,即抽象語法樹,單說抽象語法樹可能不太好理解,咱們能夠先來看一下一個具體的例子,你可使用astexplorer.net/來幫你運行@babel/parser:web
function mirror(something) {
return something
}
複製代碼
被轉譯成ast:shell
{
"type": "File",
"start": 0,
"end": 49,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 1
}
},
"program": {
"type": "Program",
"start": 0,
"end": 49,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 1
}
},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 49,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 1
}
},
"id": {
"type": "Identifier",
"start": 9,
"end": 15,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 15
},
"identifierName": "mirror"
},
"name": "mirror"
},
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 16,
"end": 25,
"loc": {
"start": {
"line": 1,
"column": 16
},
"end": {
"line": 1,
"column": 25
},
"identifierName": "something"
},
"name": "something"
}
],
"body": {
"type": "BlockStatement",
"start": 27,
"end": 49,
"loc": {
"start": {
"line": 1,
"column": 27
},
"end": {
"line": 3,
"column": 1
}
},
"body": [
{
"type": "ReturnStatement",
"start": 31,
"end": 47,
"loc": {
"start": {
"line": 2,
"column": 2
},
"end": {
"line": 2,
"column": 18
}
},
"argument": {
"type": "Identifier",
"start": 38,
"end": 47,
"loc": {
"start": {
"line": 2,
"column": 9
},
"end": {
"line": 2,
"column": 18
},
"identifierName": "something"
},
"name": "something"
}
}
],
"directives": []
}
}
],
"directives": []
},
"comments": []
}
複製代碼
乍一看彷佛很複雜,可是你要作的是從中找到關鍵信息,咱們將當中影響閱讀的字段去除(去除loc,start,end,以及函數體外層的嵌套):express
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "mirror"
},
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"name": "something"
}
],
"body": {
"type": "BlockStatement",
"body": [
{
"type": "ReturnStatement",
"argument": {
"type": "Identifier",
"name": "something"
}
}
],
"directives": []
}
}
複製代碼
這樣是否是簡單不少!咱們看一下這個json描述了什麼:外層是一個叫mirror的函數聲明,他的傳參有一個,叫something,函數體內部return了一個叫something的變量。咱們把這個描述與上邊的js代碼對照着看,居然不謀而合(其實從這一點也能看出code<=>ast這個過程是可逆的)。對於初學者而言,上邊的抽象語法樹難以理解的多是這些名字冗長的節點type,下邊簡單列舉一下js中的常見的節點名稱(慎看,能夠選擇性跳過,可是瞭解這些節點名稱能夠加深你對babel甚至js語言自己的理解)。詳見babeljs.io/docs/en/bab…編程
type | 字面含義 | 描述 |
---|---|---|
FunctionDeclaration | 函數聲明 | function a() {} |
FunctionExpression | 函數表達式 | var a = function() {} |
ArrowFunctionExpression | 箭頭函數表達式 | ()=>{} (此處能夠思考:爲何沒有箭頭函數聲明,以及Declaration和Expression的區別) |
AwaitExpression | await表達式 | async function a () { await b() } |
CallExpression | 調用表達式 | a() |
MemberExpression | 成員表達式 | a.b |
VariableDeclarator | 變量聲明 | var,const,let (var,const,let用Node中的kind區分) |
Identifier | 變量標識符 | var a (這裏a是一個Identifier) |
NumericLiteral | 數字字面量 | var a = 1 |
StringLiteral | 字符串字面量 | var a = 'a' |
BooleanLiteral | 布爾值字面量 | var a = true |
NullLiteral | null字面量 | var a = null (此處能夠思考:爲何沒有undefined字面量) |
BlockStatement | 塊 | {} |
ArrayExpression | 數組表達式 | [] |
ObjectExpression | 對象表達式 | var a = {} |
SpreadElement | 擴展運算符 | {...a},[...a] |
ObjectProperty | 對象屬性 | {a:1} (這裏的a:1是一個ObjectProperty) |
ObjectMethod | 函數屬性 | {a(){}} |
ExpressionStatement | 表達式語句 | a() |
IfStatement | if | if () {} |
ForStatement | for | for (;;){} |
ForInStatement | for in | for (a in b) {} |
ForOfStatement | for of | for (a of b) {} |
ImportDeclaration | import聲明 | import 'a' |
ImportDefaultSpecifier | import default說明符 | import a from 'a' |
ImportSpecifier | import說明符 | import {a} from 'a' |
NewExpression | new表達式 | new A() |
ClassDeclaration | class聲明 | class A {} |
ClassBody | class body | class A {} (類的內部) |
常見的列舉的差很少了。。。就先寫到這吧。
generate原本應該是第三步,爲何將第三步放到這裏呢?由於他比較簡單,並且當咱們使用traverse時,須要用到它。在這裏咱們簡單的把一段code轉換爲ast,再轉換爲code:
先安裝好依賴。這一點之後再也不贅述
yarn add @babel/parser @babel/generator
複製代碼
const parser = require('@babel/parser')
const generate = require('@babel/generator').default
const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
sourceType: 'module',
})
const transformedCode = generate(ast).code
console.log(transformedCode)
複製代碼
function mirror(something) {
return something;
}
複製代碼
這就是generator的基本用法,詳細參照babeljs.io/docs/en/bab…
到了最爲關鍵的transform步驟了,這裏的主角是@babel/traverse,@babel/types和@babel/template是輔助工具。咱們首先來談一下visitor這個概念。
訪問者是一個用於 AST 遍歷的跨語言的模式。 簡單的說它們就是一個對象,定義了用於在一個樹狀結構中獲取具體節點的方法。
假如你這樣寫了一個visitor傳遞給babel:
const visitor = {
Identifier () {
enter () {
console.log('Hello Identifier!')
},
exit () {
console.log('Bye Identifier!')
}
}
}
複製代碼
那麼babel會使用他的遞歸遍歷器去遍歷整棵ast,在進入和退出Identifier節點時,會執行咱們定義的函數。
2.在通常狀況下exit較少使用,因此能夠簡寫成:
const visitor = {
Identifier () {
console.log('Hello Identifier!')
}
}
複製代碼
3.若有必要,你還能夠把方法名用|分割成a節點類型|b節點類型形式的字符串,把同一個函數應用到多種訪問節點。
const visitor = {
'FunctionExpression|ArrowFunctionExpression' () {
console.log('A function expression or a arrow function expression!')
}
}
複製代碼
好了,如今以上邊的mirror函數爲例,來動手寫一個traverse的簡單示例吧:
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
sourceType: 'module',
})
const visitor = {
Identifier (path) {
console.log(path.node.name)
}
}
traverse(ast, visitor)
複製代碼
與你的預估是否一致呢?若是一致,那咱們能夠繼續往下。此處你可能提出疑問:這個path是什麼?
能夠簡單地認爲path是對當前訪問的node的一層包裝。例如使用path.node能夠訪問到當前的節點,使用path.parent能夠訪問到父節點,這裏列出了path所包含的內容(還沒有列出path中所包含的一些方法)。
{
"parent": {...},
"node": {...},
"hub": {...},
"contexts": [],
"data": {},
"shouldSkip": false,
"shouldStop": false,
"removed": false,
"state": null,
"opts": null,
"skipKeys": null,
"parentPath": null,
"context": null,
"container": null,
"listKey": null,
"inList": false,
"parentKey": null,
"key": null,
"scope": null,
"type": null,
"typeAnnotation": null
}
複製代碼
當你有一個 Identifier() 成員方法的訪問者時,你其實是在訪問路徑而非節點。 經過這種方式,你操做的就是節點的響應式表示(譯註:即路徑)而非節點自己。babel handbook
path中還提供了一系列的工具函數,例如traverse(在當前path下執行遞歸),remove(刪除當前節點),replaceWith(替換當前節點)等等。
解釋完了path以後,咱們試着真正的來轉換一下代碼吧,在這裏使用了@babel/generator來將ast轉換爲code
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
sourceType: 'module',
})
const visitor = {
Identifier (path) {
path.node.name = path.node.name.split('').reverse().join('')
}
}
traverse(ast, visitor)
const transformedCode = generate(ast).code
console.log(transformedCode)
複製代碼
function rorrim(gnihtemos) {
return gnihtemos;
}
複製代碼
這段代碼應該不難理解,就是將全部的變量作了個字符串翻轉。是否是事情已經變得有趣起來了?
Babel Types模塊是一個用於 AST 節點的 Lodash 式工具庫(譯註:Lodash 是一個 JavaScript 函數工具庫,提供了基於函數式編程風格的衆多工具函數), 它包含了構造、驗證以及變換 AST 節點的方法。 該工具庫包含考慮周到的工具方法,對編寫處理AST邏輯很是有用。(依然是handbook原話)
展現一下最經常使用的使用方式,用來判斷節點的類型
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const t = require('@babel/types')
const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
sourceType: 'module',
})
const visitor = {
enter(path) {
if (t.isIdentifier(path.node)) {
console.log('Identifier!')
}
}
}
traverse(ast, visitor)
複製代碼
@babel/types還能夠用來生成節點,結合上邊的知識,咱們試着改動mirror函數的返回值
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const t = require('@babel/types')
const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
sourceType: 'module',
})
const strNode = t.stringLiteral('mirror')
const visitor = {
ReturnStatement (path) {
path.traverse({
Identifier(cpath){
cpath.replaceWith(strNode)
}
})
}
}
traverse(ast, visitor)
const transformedCode = generate(ast).code
console.log(transformedCode)
複製代碼
function mirror(something) {
return "mirror";
}
複製代碼
在這裏咱們用到了t.stringLiteral('mirror')去建立一個字符串字面量節點,而後遞歸遍歷ReturnStatement下的Identifier,並將其替換成咱們所建立的字符串字面量節點(注意此處咱們已經開始使用了一些path下的公共方法)。
使用@babel/type建立一些簡單節點會很容易,可是若是是大段代碼的話就會變得困難了,這個時候咱們可使用@babel/template。下面寫了一個簡單示例,爲mirror函數內部寫了一些邏輯判斷。
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const template = require('@babel/template').default
const t = require('@babel/types')
const code = `function mirror(something) { return something }`
const ast = parser.parse(code, {
sourceType: 'module',
})
const visitor = {
FunctionDeclaration(path) {
// 在這裏聲明瞭一個模板,比用@babel/types去生成方便不少
const temp = template(` if(something) { NORMAL_RETURN } else { return 'nothing' } `)
const returnNode = path.node.body.body[0]
const tempAst = temp({
NORMAL_RETURN: returnNode
})
path.node.body.body[0] = tempAst
}
}
traverse(ast, visitor)
const transformedCode = generate(ast).code
console.log(transformedCode)
複製代碼
function mirror(something) {
if (something) {
return something;
} else {
return 'nothing';
}
}
複製代碼
完美!以上,babel基本的工具使用方式就介紹的差很少了,下邊步入正題:嘗試寫一個babel插件。
其實到這裏,編寫一個babel插件已經很是簡單了,咱們嘗試直接將上邊的代碼移植成爲一個babel插件
module.exports = function (babel) {
const {
types: t,
template
} = babel
const visitor = {
FunctionDeclaration(path) {
const temp = template(` if(something) { NORMAL_RETURN } else { return 'nothing' } `)
const returnNode = path.node.body.body[0]
const tempAst = temp({
NORMAL_RETURN: returnNode
})
path.node.body.body[0] = tempAst
}
}
return {
name: 'my-plugin',
visitor
}
}
複製代碼
babel插件暴露了一個函數,函數的傳參是babel,你可使用解構賦值獲取到types,template這些工具。函數返回值中包含一個name和一個visitor,name是插件的名稱,visitor就是咱們上邊屢次編寫的visitor。
你可能注意到了一些babel插件是能夠傳參的,那咱們如何在babel插件中接收參數呢
module.exports = function (babel) {
const {
types: t,
template
} = babel
const visitor = {
FunctionDeclaration(path, state) {
const temp = template(` if(something) { NORMAL_RETURN } else { return '${state.opts.whenFalsy}' } `)
const returnNode = path.node.body.body[0]
const tempAst = temp({
NORMAL_RETURN: returnNode
})
path.node.body.body[0] = tempAst
}
}
return {
name: 'my-plugin',
visitor
}
}
複製代碼
在上邊的例子中咱們看到在visitor中能夠傳入第二個參數state,在這個state中,使用state.opts[配置名]就可訪問到用戶所傳遞的對應配置名的值
如何測試你所編寫的babel插件是可使用的呢?引用你所編寫的插件並測試一下:
const babel = require("@babel/core")
const code = `function mirror(something) { return something }`
const res = babel.transformSync(code, {
plugins: [
[require('你編寫的插件地址'), {
whenFalsy: 'Nothing really.'
}]
]
})
console.log(res.code)
複製代碼
function mirror(something) {
if (something) {
return something;
} else {
return 'Nothing really.';
}
}
複製代碼
以上,咱們基本對babel的原理有了一個基本的認識,而且能夠本身寫出一個babel插件了。至於如何將babel的威力發揮在平常的工做中呢?就須要各位去自行探索了。
水滴前端團隊招募夥伴,歡迎投遞簡歷到郵箱:fed@shuidihuzhu.com