說到 babel 你確定會先想到 babel 能夠將還未被瀏覽器實現的 ES6 規範轉換成可以運行 ES5 規範,或者能夠將 JSX 轉換爲瀏覽器能識別的 HTML 結構,那麼 babel 是如何進行這個轉換的步驟呢,下面我將經過開發一個簡單的 babel 插件來解釋這整個過程,但願你對 Babel 插件原理與 AST 有新的認知。node
從上面的分析,咱們大概能猜出 Babel 的運行過程是:原始代碼 -> 修改代碼,那麼在這個轉換的過程當中,咱們須要知道如下三個重要的步驟。git
首先須要將 JavaScript 字符串通過詞法分析、語法分析後,轉換爲計算機更易處理的表現形式,稱之爲「抽象語法樹(AST)」,這個步驟咱們使用了 Babylon 解析器。github
當 JavaScript 從字符串轉換爲 AST 後,咱們就能更方便地對其進行瀏覽、分析和有規律的修改,根據咱們的需求,將其轉換爲新的 AST,babel-traverse 是一個很好的轉換工具,使得咱們可以很便利的操做 AST 。npm
最後,咱們將修改完的 AST 進行反向處理,生成 JavaScript 字符串,整個轉換過程也就完成了,這一步當中,咱們使用到了 babel-generator 模塊。編程
以前聽過一句話:「若是你能熟練地操做 AST ,那麼你真的能夠隨心所欲。」,當時並不理解其含義,直到真正瞭解 AST 後,才發現 AST 對編程語言的重要性是不可估量的。設計模式
在計算機科學中,抽象語法樹(abstract syntax tree 或者縮寫爲 AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式,這裏特指編程語言的源代碼。樹上的每一個節點都表示源代碼中的一種結構。瀏覽器
之因此說語法是「抽象」的,是由於這裏的語法並不會表示出真實語法中出現的每一個細節。bash
JavaScript 程序通常是由一系列字符組成的,咱們可使用匹配的字符([], {}, ()),成對的字符('', "")和縮進讓程序解析起來更加簡單,可是對計算機來講,這些字符在內存中僅僅是個數值,並不能處理這些高級問題,因此咱們須要找到一種方式,將其轉換成計算機能理解的結構。babel
咱們簡單看下面的代碼:編程語言
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 插件結構,以下代碼所示:
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。
首先,咱們須要替換的是 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 的文檔
咱們回到test.js
,運行node test.js
,便會得出:
node test.js
=> var bar = vince.init(1, 2, 3);
複製代碼
到這裏,咱們簡易的 Babel 插件便完成好了,實際上的開發需求要複雜的多,可是主要的邏輯仍是離不開上面的幾個概念。
仍是回到開始那句話「若是你能熟練地操做 AST ,那麼你真的能夠隨心所欲。」,咱們可以經過 AST 將原始代碼轉換成咱們所須要的任何代碼,甚至你能建立一個私人的 ESXXX
,添加你創造的新規範。AST 並非一個很複雜的技術活,很大一部分能夠視爲「苦力活」,由於遇到複雜的轉換需求可能須要編寫寫不少邏輯代碼。
經過閱讀這篇文章,咱們瞭解了 Babel 插件的實現原理,而且實踐了一個 Plugin,除此以外,咱們也理解了 AST 的概念,認識到了其強大之處。
引用: