前言
說到 babel 你肯定會先想到 babel 可以將還未被瀏覽器實現的 ES6 規範轉換成能夠執行 ES5 規範,或者可以將 JSX 轉換為瀏覽器能識別的 HTML 結構,那麼 babel 是如何進行這個轉換的步驟呢,下面我將通過開發一個簡單的 babel 外掛來解釋這整個過程,希望你對 Babel 外掛原理與 AST 有新的認知。
Babel 執行階段
從上面的分析,我們大概能猜出 Babel 的執行過程是:原始程式碼 -> 修改程式碼,那麼在這個轉換的過程中,我們需要知道以下三個重要的步驟。
解析
首先需要將 JavaScript 字串經過詞法分析、語法分析後,轉換為計算機更易處理的表現形式,稱之為“抽象語法樹(AST)”,這個步驟我們使用了 Babylon 解析器。
轉換
當 JavaScript 從字串轉換為 AST 後,我們就能更方便地對其進行瀏覽、分析和有規律的修改,根據我們的需求,將其轉換為新的 AST,babel-traverse 是一個很好的轉換工具,使得我們能夠很便利的操作 AST 。
生成
最後,我們將修改完的 AST 進行反向處理,生成 JavaScript 字串,整個轉換過程也就完成了,這一步當中,我們使用到了 babel-generator 模組。
什麼是 AST
之前聽過一句話:“如果你能熟練地操作 AST ,那麼你真的可以為所欲為。”,當時並不理解其含義,直到真正瞭解 AST 後,才發現 AST 對程式語言的重要性是不可估量的。
在電腦科學中,抽象語法樹(abstract syntax tree 或者縮寫為 AST),或者語法樹(syntax tree),是原始碼的抽象語法結構的樹狀表現形式,這裡特指程式語言的原始碼。樹上的每個節點都表示原始碼中的一種結構。
之所以說語法是「抽象」的,是因為這裡的語法並不會表示出真實語法中出現的每個細節。
JavaScript 程式一般是由一系列字元組成的,我們可以使用匹配的字元([], {}, ()),成對的字元('', "")和縮排讓程式解析起來更加簡單,但是對計算機來說,這些字元在記憶體中僅僅是個數值,並不能處理這些高階問題,所以我們需要找到一種方式,將其轉換成計算機能理解的結構。
我們簡單看下面的程式碼:
let a = 2;
a * 8
複製程式碼
將其轉換為 AST 會是怎樣的呢,我們使用 astexplorer 線上 AST 轉換工具,可以得到以下樹結構:
為了更形象表述,我們將其轉換為更直觀的結構圖形:
AST 的根節點都是 Program ,這個例子中包含了兩部分:
-
一個變數申明(VariableDeclarator),將識別符號(Identifier) a 賦值為數值(NumericLiteral) 3。
-
一個二元表示式語句(BinaryExpression),描述為標誌符(Identifier)為 a,操作符(operator) + 和數值(NumericLiteral) 5。
這只是一個簡單的例子,在實際開發中,AST 將會是一個巨型節點樹,將字串形式的原始碼轉換成樹狀的結構,計算機便能更方便地處理,我們使用的 Babel 外掛,也就是對 AST 進行插入/移動/替換/刪除節點,建立成新的 AST ,再將 AST 轉換為字串原始碼,這便是 Babel 外掛的原理,之所以能夠“為所欲為”,其原因就是可以將原始程式碼按照指定邏輯轉換為你想要的程式碼。
開發 Babel 外掛 Demo
基礎概念
一個典型的 Babel 外掛結構,如下程式碼所示:
export default function(babel) {
var t = babel.types;
return {
visitor: {
ArrayExpression(path, state) {
path.replaceWith(
t.callExpression(
t.memberExpression(t.identifier('mori'), t.identifier('vector')),
path.node.elements
)
);
},
ASTNodeTypeHere(path, state) {}
}
};
};
複製程式碼
我們要關注的幾個點為:
babel.types
: 用來操作 AST 節點,如建立、轉換、校驗等。vistor
: Babel 採用遞迴的方式訪問 AST 的每個節點,之所以叫做visitor,只是因為有個類似的設計模式叫做訪問者模式,如上述程式碼中的ArrayExpression
,當遍歷到ArrayExpression
節點時,即觸發對應函式。path
: path 是指 AST 節點的物件,可以用來獲取節點的屬性、節點之間的關聯。state
: 指外掛的狀態,可以用過 state 來獲取外掛中的配置項。ArrayExpression、ASTNodeTypeHere
: 指 AST 中的節點型別。
需求分析
因為是 Demo ,我們需求很簡單,我們開發的 Bable 外掛名稱叫 vincePlugin
,在使用的時候,能配置外掛的引數,使得外掛能按照我們配置的引數進行轉換。
// babel 引數配置
plugins: [
[vincePlugin, {
name: 'vince'
}]
]
複製程式碼
轉換效果:
var fool = [1,2,3];
// translate to =>
var fool = vince.init(1,2,3)
複製程式碼
初始化專案
為了大家更方便的閱讀程式碼,原始碼已經上傳到GitHub: babel-plugin-demo
瞭解了以上概念與需求後,我們就可以開始進行 Babel 外掛開發,開始之前先建立一個專案目錄,初始化 npm ,並安裝 babel-core :
mkdir babel-plugin-demo && cd babel-plugin-demo
npm init -y
npm install --save-dev babel-core
複製程式碼
建立 plugin.js
babel 外掛檔案,我們將會在這裡寫轉換的邏輯程式碼:
// plugin.js
module.exports = function(babel) {
var t = babel.types;
return {
visitor: {
// ...
}
};
};
複製程式碼
建立原始程式碼 index.js
var fool = [1,2,3];
複製程式碼
建立 test.js
測試函式,這裡我們進行對外掛的測試:
// test.js
var fs = require('fs');
var babel = require('babel-core');
var vincePlugin = require('./plugin');
// read the code from this file
fs.readFile('index.js', function(err, data) {
if(err) throw err;
// convert from a buffer to a string
var src = data.toString();
// use our plugin to transform the source
var out = babel.transform(src, {
plugins: [
[vincePlugin, {
name: 'vince'
}]
]
});
// print the generated code to screen
console.log(out.code);
});
複製程式碼
我們通過 node test.js
,來測試 babel 外掛的轉換輸出。
節點對比
- 原始程式碼
var fool = [1,2,3];
通過 AST 分析出來的節點如圖:
- 轉換後程式碼
var bar = vince.init(1, 2, 3);
,通過 AST 分析出來的節點如圖:
我們通過用紅色標註來區分原始與轉換後的 AST 結構圖,現在我們可以很清晰的看到我們需要替換的節點,將 ArrayExpression 替換為 CallExpression ,在 CallExpression 節點中中增加一個 MemberExpression,並且保留原始的三個 NumericLiteral。
plugin 編寫
首先,我們需要替換的是 ArrayExpression ,所以給 vistor 新增 ArrayExpression 方法。
// plugin.js
module.exports = function(babel) {
var t = babel.types;
return {
visitor: {
ArrayExpression: function(path, state) {
// ...
}
}
};
};
複製程式碼
當 Babel 遍歷 AST 時,當發現含有 visitor 上有對呀節點方法時,即會觸發這個方法,並且將上下文傳入(path, state),在函式裡面我們進行節點的分析和替換操作:
// plugin.js
module.exports = function(babel) {
var t = babel.types;
return {
visitor: {
ArrayExpression: function(path, state) {
// 替換該節點
path.replaceWith(
// 建立一個 callExpression
t.callExpression(
t.memberExpression(t.identifier(state.opts.name), t.identifier('init')),
path.node.elements
)
);
}
}
};
};
複製程式碼
我們需要將 ArrayExpression 替換為 CallExpression,可以通過 t.callExpression(callee, arguments) 來生成 CallExpression,第一個引數是 MemberExpression,通過t.memberExpression(object, property) 來生成,然後再將原有的三個 NumericLiteral 設定為第二個引數,於是就完成了我們的需求。
這裡我們要注意 state.opts.name
中指的是配置 plugin 時,設定的 config 引數。
更多的轉換方式和節點屬性,可以查閱 babel-types 的文件
測試plugin
我們回到test.js
,執行node test.js
,便會得出:
node test.js
=> var bar = vince.init(1, 2, 3);
複製程式碼
到這裡,我們簡易的 Babel 外掛便完成好了,實際上的開發需求要複雜的多,但是主要的邏輯還是離不開上面的幾個概念。
總結
還是回到開始那句話“如果你能熟練地操作 AST ,那麼你真的可以為所欲為。”,我們能夠通過 AST 將原始程式碼轉換成我們所需要的任何程式碼,甚至你能建立一個私人的 ESXXX
,新增你創造的新規範。AST 並不是一個很複雜的技術活,很大一部分可以視為“苦力活”,因為遇到複雜的轉換需求可能需要編寫寫很多邏輯程式碼。
通過閱讀這篇文章,我們瞭解了 Babel 外掛的實現原理,並且實踐了一個 Plugin,除此之外,我們也理解了 AST 的概念,認識到了其強大之處。
引用: