前言
之前看到一位大佬的部落格, 介紹了babel的原理, 以及如何寫一個babel的外掛, 抱著試試看的想法, 照葫蘆畫瓢的自己寫了一個簡單的babel外掛, 該外掛的作用就是將程式碼字串中的表示式, 直接轉換為對應的計算結果。例如: const code = const result = 1 + 1
轉化為const code = const result = 2
。當然這一篇文章非常的淺顯, 但是對了解Babel的原理以及AST的基本概念是足夠的了。
相關連結
外掛的原始碼
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
}
}
複製程式碼
基本概念
建議先閱讀一下這一篇文件
babel工作的原理
Babel對程式碼進行轉換,會將JS程式碼轉換為AST抽象語法樹(解析),對樹進行靜態分析(轉換),然後再將語法樹轉換為JS程式碼(生成)。每一層樹被稱為節點。每一層節點都會有type屬性,用來描述節點的型別。其他屬性用來進一步描述節點的型別。
// 將程式碼生成對應的抽象語法樹
// 程式碼
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抽象語法樹
轉換
節點的路徑(path)物件上, 會暴露很多新增, 刪除, 修改AST的API, 通過操作這些API實現對AST的修改
生成
生成則是通過對修改後的AST的遍歷, 生成新的原始碼
遍歷
AST是樹形的結構, AST的轉換的步驟就是通過訪問者對AST的遍歷實現的。訪問者會定義處理不同的節點型別的方法。遍歷樹形結構的同時,, 遇到對應的節點型別會執行相對應的方法。
訪問者
Visitors訪問者本身就是一個物件,物件上不同的屬性, 對應著不同的AST節點型別。例如,AST擁有BinaryExpression(二元表示式)型別的節點, 如果在訪問者上定義BinaryExpression屬性名的方法, 則這個方法在遇到BinaryExpression型別的節點, 就會執行, BinaryExpression方法的引數則是該節點的路徑。注意對每一個節點的遍歷會執行兩次, 進入節點一次, 退出節點一次
const visitors = {
enter (path) {
// 進入該節點
},
exit (path) {
// 退出該節點
}
}
複製程式碼
路徑
每一個節點都擁有自身的路徑物件(訪問者的引數, 就是該節點的路徑物件), 路徑物件上定義了不同的屬性和方法。例如: path.node代表了該節點的子節點, path.parent則代表了該節點的父節點。path.replaceWithMultiple方法則定義的是替換該節點的方法。
訪問者中的路徑
節點的路徑資訊, 存在於訪問者的引數中, 訪問者的預設的引數就是節點的路徑物件
第一個外掛
我們來寫一個將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函式的處理可以參考上面的原始碼.