Babel 是一個 JavaScript 編譯器。node
// Babel 輸入: ES2015 箭頭函數
[1, 2, 3].map((n) => n + 1);
// Babel 輸出: ES5 語法實現的同等功能
[1, 2, 3].map(function(n) {
return n + 1;
});
複製代碼
Babel經過轉換,讓咱們寫新版本的語法,轉換到低版本,這樣就能夠在只支持低版本語法的瀏覽器裏運行了。git
Babel真厲害,它竟然‘認識’代碼、更改代碼。那Babel就是操做代碼的代碼,酷。github
學習Babel對咱們能力的提高有很大幫助,咱們平時都是用代碼來操做各類東西,此次咱們操做的對象,變成了代碼自己。算法
這個認識和操做代碼的過程,學名叫作代碼的靜態分析。express
先來看一個問題,編輯器裏代碼的高亮,以下:npm
編輯器能夠把代碼不一樣的成員,標記爲不一樣的顏色。顯然編輯器要‘認識’代碼,對代碼進行分析和處理,才能達到這種效果。這就叫代碼的靜態分析。瀏覽器
靜態分析是在不須要執行代碼的前提下對代碼進行分析的處理過程。bash
動態分析是在代碼的運行過程當中對代碼進行分析和處理。上面的代碼高亮屬於靜態分析,在代碼沒有運行的狀況下,進行分析和處理的。babel
靜態分析不光能高亮代碼,還有就是代碼轉換,還能夠對咱們的源代碼進行優化、壓縮等操做。數據結構
在對代碼靜態分析的過程當中,要將源碼轉換成AST (抽象語法樹)。
源代碼對於Babel來講,就是一個字符串。Babel要對這個字符串進行分析。咱們平時對字符串的操做,就是使用字符串方法或是正則,但相對字符串(源碼)進行復雜的操做,遠遠不夠。
須要將字符串(源碼)轉換成樹的數據結構,纔好操做。這個樹結構,就叫AST(抽象語法樹)。
這裏我想到 程序 = 數據結構 + 算法。咱們平時寫的業務需求,對數據結構要求不高,簡單的對象和列表就能夠搞定,但要某些特定的複雜問題,好比如今研究的操做代碼,就需先思考:我應該把操做的事物放到什麼樣的數據結構上,才更容易我寫算法/邏輯。
對於源碼,此時咱們就把它看出一個字符串,對其分析的第一步,確定是先把源碼轉換成AST,纔好後續操做。
有一個在線AST轉換器,咱們在這上面能夠作實驗,寫出的代碼,它就幫咱們翻譯成AST:
我什麼都不寫,AST就有一個根結點了:
// AST
{
"type": "Program",
"start": 0,
"end": 0,
"body": [],
"sourceType": "module"
} // 能夠當作是一個對象,有一些字段,這代碼樹的根結點。
複製代碼
而後我寫一句代碼:
// 源碼
const text = 'Hello World';
// AST
{
"type": "Program",
"start": 0,
"end": 27,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 27,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 26,
"id": {
"type": "Identifier",
"start": 6,
"end": 10,
"name": "text"
},
"init": {
"type": "Literal",
"start": 13,
"end": 26,
"value": "Hello World",
"raw": "'Hello World'"
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
複製代碼
懵逼,一句const text = 'Hello World'; 就生成這麼多東西。看懂它、理解AST是學習Babel的第一個門檻。
看下圖,這就是AST Explorer的界面,左邊寫代碼,右邊就幫助咱們翻譯成AST,AST有兩種表達方式,Tree和JSON,上面都是用JSON形式表示AST,後來我發現仍是用Tree的形式看更容易些,由於Tree的形式更突出節點的類型:
我將AST表達的樹畫出來,以下:
總結AST樹的特色:
但願能從上面的分析中,讓你們對AST有一個最直觀的認識,就是節點有類型的樹。
那麼節點的類型系統就很必要了解了,這裏是Babel的AST類型系統說明。你們能夠看看,能夠說類型系統是抽象了代碼的各類成員,標識符、字面量、聲明、表達式。因此擁有這些類型的節點的樹結構,能夠用來表達咱們的代碼。
參照類型系統,多實驗,咱們就會對AST的結構大致掌握和理解了。
額外提一下,V8中也用到了AST。V8引擎有四個主要模塊:
能夠看到AST也是V8執行的關鍵一環。下面下來看看Babel對於AST的利用,及運行步驟。
回看Babel的處理過程
第一步:解析,Babel中的解析器是babylon。咱們來體驗一下:
// 安裝
npm install --save babylon
複製代碼
// 實驗代碼
import * as babylon from "babylon";
const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);
console.log('ast', ast);
複製代碼
code變量是咱們的源代碼,ast變量是AST,咱們看一下打印結果:
和咱們預期的同樣,獲得AST了。這裏我注意到還有start、end、loc這樣位置信息的字段,應該能夠對生成Source Map有用的字段。
第二步:轉換。獲得ast了,該操做它了,Babel中的babel-traverse用來幹這個事。
// 安裝
npm install --save babel-traverse
複製代碼
// 實驗代碼
import * as babylon from "babylon";
import traverse from "babel-traverse";
const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);
traverse(ast, {
enter(path) {
console.log('path', path);
}
})
console.log('ast', ast);
複製代碼
babel-traverse庫暴露了traverse方法,第一個參數是ast,第二個參數是一個對象,咱們寫了一個enter方法,方法的參數是個path,咋不是個node呢?咱們看一下輸出:
path被打印了5次,ast上確實也是有5個節點,是對應的。traverse方法是一個遍歷方法,path封裝了每個節點,而且還提供容器container,做用域scope這樣的字段。提供個更多關於節點的相關的信息,讓咱們更好的操做節點。
咱們來作一個變量重命名操做:
import * as babylon from "babylon";
import traverse from "babel-traverse";
const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);
traverse(ast, {
enter(path) {
if (path.node.type === "Identifier"
&& path.node.name === 'text') {
path.node.name = 'alteredText';
}
}
})
console.log('ast', ast);
複製代碼
看結果:
確實咱們的ast被更改了,用這個ast生成的code就會是const alteredText = 'Hello World';
在利用babel-traverse操做AST時,也能夠利用工具庫幫助咱們寫出更加簡潔有效的代碼,就可使用babel-types。
npm install --save babel-types
複製代碼
import * as babylon from "babylon";
import traverse from "babel-traverse";
import * as t from "babel-types";
const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);
traverse(ast, {
enter(path) {
// 使用babel-types
if (t.isIdentifier(path.node, { name: "text" })) {
path.node.name = 'alteredText';
}
}
})
console.log('ast', ast);
複製代碼
使用babel-types實現和上例中同樣的功能,代碼量更少了。
第三步:生成。獲得操做後的ast,該生成新代碼了。Babel中的babel-generator用來幹這個事。
npm install --save babel-generator
複製代碼
// 加入babel-generator
import * as babylon from "babylon";
import traverse from "babel-traverse";
import * as t from "babel-types";
import generate from "babel-generator";
const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);
traverse(ast, {
enter(path) {
if (t.isIdentifier(path.node, { name: "text" })) {
path.node.name = 'alteredText';
}
}
})
const genCode = generate(ast, {}, code);
console.log('genCode', genCode);
複製代碼
來看打印結果:
nice! 在code字段裏,咱們看到裏新生成的代碼。
固然,上面提到的這四個庫,還有更多細節,有興趣的能夠再研究研究。
這些庫的集合,就是咱們的Babel。
咱們研究過了Babel的內部,如今跳出來,咱們須要在外部操做AST,而不是進Babel內部去改traverse。
從外部操做AST,就須要插件了。咱們來研究一個插件babel-plugin-transform-member-expression-literals
使用次插件後:
// 轉換前
obj.const = "isKeyword";
// 轉換後
obj["const"] = "isKeyword";
複製代碼
咱們再來看看這個插件的源碼:
{
name: "transform-member-expression-literals",
visitor: {
MemberExpression: {
exit({ node }) {
const prop = node.property;
if (
!node.computed &&
t.isIdentifier(prop) &&
!t.isValidES3Identifier(prop.name)
) {
// foo.default -> foo["default"]
node.property = t.stringLiteral(prop.name);
node.computed = true;
}
},
},
},
};
}
複製代碼
這裏我嘗試將它放到咱們的實驗代碼裏,以下:
import * as babylon from "babylon";
import traverse from "babel-traverse";
import * as t from "babel-types";
import generate from "babel-generator";
const code = `const obj = {};obj.const = "isKeyword";`;
const ast = babylon.parse(code);
const plugin = {
MemberExpression: {
exit({ node }) {
const prop = node.property;
console.log('node', node);
if (
!node.computed &&
t.isIdentifier(prop)
// !t.isValidES3Identifier(prop.name) 這裏註釋掉,咱們的t裏沒這個方法
) {
// foo.default -> foo["default"]
node.property = t.stringLiteral(prop.name);
node.computed = true;
}
},
},
};
traverse(ast, plugin)
const genCode = generate(ast, {}, code);
console.log('genCode', genCode);
複製代碼
輸出的代碼是"const obj = {};obj["const"] = "isKeyword";"。符合預期,也就是說,Babel的插件,會傳給內部的traverse方法。而且是一種符合訪問者模式的,讓咱們能夠針對節點類型(如這裏的visitor.MemberExpression)的操做。這裏用的是exit,而不是enter了,解釋一下,traverse是對樹的深度遍歷,向下遍歷這棵樹咱們進入(entry)每一個節點,向上遍歷回去時咱們退出(exit)每一個節點。就是對於AST,traverse遍歷了兩遍,咱們能夠選擇在進入仍是退出的時候,操縱節點。
個人參考:
今天的研究就到這裏,理解Babel內部機制和基本的插件工做方式。