以前看到一位大佬的博客, 介紹了babel的原理, 以及如何寫一個babel的插件, 抱着試試看的想法, 照葫蘆畫瓢的本身寫了一個簡單的babel插件, 該插件的做用就是將代碼字符串中的表達式, 直接轉換爲對應的計算結果。例如: const code = const result = 1 + 1
轉化爲const code = const result = 2
。固然這一篇文章很是的淺顯, 可是對了解Babel的原理以及AST的基本概念是足夠的了。node
const t = require('babel-types')
const visitor = {
// 二元表達式類型節點的訪問者
BinaryExpression(path) {
// 子節點
// 訪問者會一層層遍歷AST抽象語法樹, 會樹形遍歷AST的BinaryExpression類型的節點
const childNode = path.node
let result = null
if (
t.isNumericLiteral(childNode.left) &&
t.isNumericLiteral(childNode.right)
) {
const operator = childNode.operator
switch (operator) {
case '+':
result = childNode.left.value + childNode.right.value
break
case '-':
result = childNode.left.value - childNode.right.value
break
case '/':
result = childNode.left.value / childNode.right.value
break
case '*':
result = childNode.left.value * childNode.right.value
break
}
}
if (result !== null) {
// 替換本節點爲數字類型
path.replaceWith(
t.numericLiteral(result)
)
if (path.parentPath) {
const parentType = path.parentPath.type
if (visitor[parentType]) {
visitor[parentType](path.parentPath)
}
}
}
},
// 屬性表達式
MemberExpression(path) {
const childNode = path.node
let result = null
if (
t.isIdentifier(childNode.object) &&
t.isIdentifier(childNode.property) &&
childNode.object.name === 'Math'
) {
result = Math[childNode.property.name]
}
if (result !== null) {
const parentType = path.parentPath.type
if (parentType !== 'CallExpression') {
// 替換本節點爲數字類型
path.replaceWith(
t.numericLiteral(result)
)
if (visitor[parentType]) {
visitor[parentType](path.parentPath)
}
}
}
},
// 一元表達式
UnaryExpression (path) {
const childNode = path.node
let result = null
if (
t.isLiteral(childNode.argument)
) {
const operator = childNode.operator
switch (operator) {
case '+':
result = childNode.argument.value
break
case '-':
result = -childNode.argument.value
break
}
}
if (result !== null) {
// 替換本節點爲數字類型
path.replaceWith(
t.numericLiteral(result)
)
if (path.parentPath) {
const parentType = path.parentPath.type
if (visitor[parentType]) {
visitor[parentType](path.parentPath)
}
}
}
},
// 函數執行表達式
CallExpression(path) {
const childNode = path.node
// 結果
let result = null
// 參數的集合
let args = []
// 獲取函數的參數的集合
args = childNode.arguments.map(arg => {
if (t.isUnaryExpression(arg)) {
return arg.argument.value
}
})
if (
t.isMemberExpression(childNode.callee)
) {
if (
t.isIdentifier(childNode.callee.object) &&
t.isIdentifier(childNode.callee.property) &&
childNode.callee.object.name === 'Math'
) {
result = Math[childNode.callee.property.name].apply(null, args)
}
}
if (result !== null) {
// 替換本節點爲數字類型
path.replaceWith(
t.numericLiteral(result)
)
if (path.parentPath) {
const parentType = path.parentPath.type
if (visitor[parentType]) {
visitor[parentType](path.parentPath)
}
}
}
}
}
module.exports = function () {
return {
visitor
}
}
複製代碼
建議先閱讀一下這一篇文檔git
Babel對代碼進行轉換,會將JS代碼轉換爲AST抽象語法樹(解析),對樹進行靜態分析(轉換),而後再將語法樹轉換爲JS代碼(生成)。每一層樹被稱爲節點。每一層節點都會有type屬性,用來描述節點的類型。其餘屬性用來進一步描述節點的類型。github
// 將代碼生成對應的抽象語法樹
// 代碼
const result = 1 + 1
// 代碼生成的AST
{
"type": "Program",
"start": 0,
"end": 20,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 20,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 20,
"id": {
"type": "Identifier",
"start": 6,
"end": 12,
"name": "result"
},
"init": {
"type": "BinaryExpression",
"start": 15,
"end": 20,
"left": {
"type": "Literal",
"start": 15,
"end": 16,
"value": 1,
"raw": "1"
},
"operator": "+",
"right": {
"type": "Literal",
"start": 19,
"end": 20,
"value": 1,
"raw": "1"
}
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
複製代碼
解析分爲詞法解析和語法分析, 詞法解析將代碼字符串生成令牌流, 而語法分析則會將令牌流轉換成AST抽象語法樹babel
節點的路徑(path)對象上, 會暴露不少添加, 刪除, 修改AST的API, 經過操做這些API實現對AST的修改app
生成則是經過對修改後的AST的遍歷, 生成新的源碼函數
AST是樹形的結構, AST的轉換的步驟就是經過訪問者對AST的遍歷實現的。訪問者會定義處理不一樣的節點類型的方法。遍歷樹形結構的同時,, 遇到對應的節點類型會執行相對應的方法。post
Visitors訪問者自己就是一個對象,對象上不一樣的屬性, 對應着不一樣的AST節點類型。例如,AST擁有BinaryExpression(二元表達式)類型的節點, 若是在訪問者上定義BinaryExpression屬性名的方法, 則這個方法在遇到BinaryExpression類型的節點, 就會執行, BinaryExpression方法的參數則是該節點的路徑。注意對每個節點的遍歷會執行兩次, 進入節點一次, 退出節點一次ui
const visitors = {
enter (path) {
// 進入該節點
},
exit (path) {
// 退出該節點
}
}
複製代碼
每個節點都擁有自身的路徑對象(訪問者的參數, 就是該節點的路徑對象), 路徑對象上定義了不一樣的屬性和方法。例如: path.node表明了該節點的子節點, path.parent則表明了該節點的父節點。path.replaceWithMultiple方法則定義的是替換該節點的方法。spa
節點的路徑信息, 存在於訪問者的參數中, 訪問者的默認的參數就是節點的路徑對象.net
咱們來寫一個將const result = 1 + 1
字符串解析爲const result = 2
的簡單插件。咱們首先觀察這段代碼的AST, 以下。
咱們能夠看到BinaryExpression類型(二元表達式類型)的節點, 中定義了這段表達式的主體(1 + 1), 1 分別是BinaryExpression節點的子節點left,BinaryExpression節點的子節點right,而加號則是BinaryExpression節點的operator的子節點
// 通過簡化以後
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "result"
},
"init": {
"type": "BinaryExpression",
"left": {
"type": "Literal",
"value": 1
},
"operator": "+",
"right": {
"type": "Literal",
"value": 1
}
}
}
]
}
]
}
複製代碼
接下來咱們來處理這個類型的節點,代碼以下
const t = require('babel-types')
const visitor = {
BinaryExpression(path) {
// BinaryExpression節點的子節點
const childNode = path.node
let result = null
if (
// isNumericLiteral是babel-types上定義的方法, 用來判斷節點的類型
t.isNumericLiteral(childNode.left) &&
t.isNumericLiteral(childNode.right)
) {
const operator = childNode.operator
// 根據不一樣的操做符, 將left.value, right.value處理爲不一樣的結果
switch (operator) {
case '+':
result = childNode.left.value + childNode.right.value
break
case '-':
result = childNode.left.value - childNode.right.value
break
case '/':
result = childNode.left.value / childNode.right.value
break
case '*':
result = childNode.left.value * childNode.right.value
break
}
}
if (result !== null) {
// 計算出結果後
// 將自己的節點,替換爲數字類型的節點
path.replaceWith(
t.numericLiteral(result)
)
}
}
}
複製代碼
咱們定義一個訪問者, 在上面定義BinaryExpression的屬性的方法。運行結果如咱們預期, const result = 1 + 1被處理爲了const result = 2。可是咱們將代碼修改成const result = 1 + 2 + 3發現結果變爲了 const result = 3 + 3, 這是爲何呢? 咱們來看一下1 + 2 + 3的AST抽象語法樹.
// 通過簡化的AST
type: 'BinaryExpression'
- left
- left
- left
type: 'Literal'
value: 1
- opeartor: '+'
- right
type: 'Literal'
value: 2
- opeartor: '+'
- right
type: 'Literal'
value: 3
複製代碼
咱們上面的代碼的判斷條件是。t.isNumericLiteral(childNode.left) && t.isNumericLiteral(childNode.right), 在這裏只有最裏層的AST是知足條件的。由於整個AST結構相似於, (1 + 2) + 3 => (left + rigth) + right。
解決辦法是,將內部的 1 + 2的節點替換成數字節點3以後,將數字節點3的父路徑(parentPath)從新執行BinaryExpression的方法(數字類型的3節點和right節點), 經過遞歸的方式,替換全部的節點。修改後的代碼以下。
BinaryExpression(path) {
const childNode = path.node
let result = null
if (
t.isNumericLiteral(childNode.left) &&
t.isNumericLiteral(childNode.right)
) {
const operator = childNode.operator
switch (operator) {
case '+':
result = childNode.left.value + childNode.right.value
break
case '-':
result = childNode.left.value - childNode.right.value
break
case '/':
result = childNode.left.value / childNode.right.value
break
case '*':
result = childNode.left.value * childNode.right.value
break
}
}
if (result !== null) {
// 替換本節點爲數字類型
path.replaceWith(
t.numericLiteral(result)
)
BinaryExpression(path.parentPath)
}
}
複製代碼
結果如咱們預期, const result = 1 + 2 + 3 能夠被正常的解析。可是這個插件還不具有對Math.abs(), Math.PI, 有符號的數字的處理,咱們還須要在訪問者上定義更多的屬性。最後, 對於Math.abs函數的處理能夠參考上面的源碼.