不太喜歡上來就講大道理,先來個小栗子,作個簡單而又實用的功能,作完後,理論你就理解一大半了。html
咱們須要antd裏面的一個組件Button,代碼以下:node
import { Button } from 'antd'複製代碼
咱們只想引入Button組件,如今卻導入antd/lib/index.js全部組件,致使打包後文件大了好多,這顯然不是咱們想要的。git
咱們想要的是隻引入antd/lib/buttongithub
import Button from 'antd/lib/button'複製代碼
可是這樣寫又過於麻煩,多個組件得寫不少行,每行都有個相同的antd/lib代碼冗餘,又不雅觀又很差維護。npm
咱們可使用Babel插件來解決這個問題。(完整的代碼示例)編程
創建好以下目錄結構json
- root
- node_modules
- babel-plugin-import
- index.js
- package.json
- test.js複製代碼
test.js文件內容:api
const babel = require('babel-core');
// 轉換前的代碼
const code = `import { Button } from 'antd'`;
var result = babel.transform(code, {
plugins:["import"]
});
// 轉換後的代碼
console.log(result.code);
// 輸出:import Button from 'antd/lib/button'複製代碼
babel-core是babel的核心包,transform函數能夠傳入code,返回轉換後的代碼,ast語法樹和source-map映射地圖。具體API參考 babel-core數組
package.json的main屬性指向index.js,不解釋babel
好了,開始編寫咱們的插件吧,index.js文件內容:
/* 單詞首字母小寫 */
const toLowerCase = word =>
Array.from(word).map((char, index) =>
!index ? char.toLowerCase() : char).join('')
module.exports = ({ types }) => (
{
// 插件入口
visitor: {
// 處理類型: Import聲明
ImportDeclaration(path) {
const { node } = path
const { type, specifiers, source } = node
// 過濾掉默認引用,如 import antd from 'antd'
const importSpecifiers = specifiers.filter(specifier =>
types.isImportSpecifier(specifier))
// 例子只處理了一個組件的引用
if (importSpecifiers.length === 1) {
const [importSpecifier] = importSpecifiers
// 小寫處理組件引用,如Import { Button },改成: antd/lib/button
const element = toLowerCase(importSpecifier.imported.name)
// 引入改成默認引入,如Import { Button }, 改成: import Button
importSpecifier.type = 'ImportDefaultSpecifier'
source.value += `/lib/${element}`
}
}
}
})複製代碼
短短的幾行代碼,即便看了註釋,相信親愛的讀者仍然一頭霧水。請登陸 ast語法樹在線生成網站,輸入import Button from 'antd'
,對比右側生成的語法樹與代碼,咱們從代碼開始講起。
import
這樣的代碼(請注意網站生成的語法樹)。咱們拿到specifiers是引用聲明,就是import { Button }
中的Button,而且判斷長度爲1(長度大於1的話得多行引入,要修改語法樹結構)。作了如下兩件事:
source.value
也就是'antd'
後面。ImportSpecifier
也就是{ Button }
改成ImportDefaultSpecifier
,這樣就把 Import { Button }
改爲了Import Button
。OK,就這麼簡單,也許你尚未明白,不要緊,聯繫我吧。(mail: hongji_12313@163.com)
而後咱們再來看理論(理論的例子所有在ast倉庫中)。
Babel 是 JavaScript 靜態分析編譯器,更確切地說是源碼到源碼的編譯器,一般也叫作「轉換編譯器(transpiler)」。 意思是說你爲 Babel 提供一些 JavaScript 代碼,Babel 更改這些代碼,而後返回給你新生成的代碼。
靜態分析是在不須要執行代碼的前提下對代碼進行分析的處理過程 (執行代碼的同時進行代碼分析便是動態分析)。 靜態分析的目的是多種多樣的, 它可用於語法檢查,編譯,代碼高亮,代碼轉換,優化,壓縮等等場景。複製代碼
這個處理過程當中的每一步都涉及到建立或是操做抽象語法樹,亦稱 AST。
Babel 使用一個基於 ESTree 並修改過的 AST,它的內核說明文檔能夠在這裏找到。
function square(n) {
return n * n;
}複製代碼
AST Explorer 可讓你對 AST 節點有一個更好的感性認識。 這裏是上述代碼的一個示例連接。
一樣的程序能夠表述爲下面的列表:
- FunctionDeclaration:
- id:
- Identifier:
- name: square
- params [1]
- Identifier
- name: n
- body:
- BlockStatement
- body [1]
- ReturnStatement
- argument
- BinaryExpression
- operator: *
- left
- Identifier
- name: n
- right
- Identifier
- name: n複製代碼
或是以下所示的 JavaScript Object(對象):
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
params: [{
type: "Identifier",
name: "n"
}],
body: {
type: "BlockStatement",
body: [{
type: "ReturnStatement",
argument: {
type: "BinaryExpression",
operator: "*",
left: {
type: "Identifier",
name: "n"
},
right: {
type: "Identifier",
name: "n"
}
}
}]
}
}複製代碼
你會留意到 AST 的每一層都擁有相同的結構:
{
type: "FunctionDeclaration",
id: {...},
params: [...],
body: {...}
}
{
type: "Identifier",
name: ...
}
{
type: "BinaryExpression",
operator: ...,
left: {...},
right: {...}
}複製代碼
注意:出於簡化的目的移除了某些屬性複製代碼
這樣的每一層結構也被叫作 節點(Node)。 一個 AST 能夠由單一的節點或是成百上千個節點構成。 它們組合在一塊兒能夠描述用於靜態分析的程序語法。
每個節點都有以下所示的接口(Interface):
interface Node {
type: string;
}複製代碼
字符串形式的 type 字段表示節點的類型(如: "FunctionDeclaration","Identifier",或 "BinaryExpression")。 每一種類型的節點定義了一些附加屬性用來進一步描述該節點類型。
Babel 還爲每一個節點額外生成了一些屬性,用於描述該節點在原始代碼中的位置。
{
type: ...,
start: 0,
end: 38,
loc: {
start: {
line: 1,
column: 0
},
end: {
line: 3,
column: 1
}
},
...
}複製代碼
每個節點都會有 start,end,loc 這幾個屬性。
Babel 的三個主要處理步驟分別是: 解析(parse),轉換(transform),生成(generate)。.
解析步驟接收代碼並輸出 AST。 這個步驟分爲兩個階段:詞法分析(Lexical Analysis) 和 語法分析(Syntactic Analysis)。.
詞法分析階段把字符串形式的代碼轉換爲 令牌(tokens) 流。.
你能夠把令牌看做是一個扁平的語法片斷數組:
n * n;
[
{ type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
...
]複製代碼
每個 type 有一組屬性來描述該令牌:
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
...
}複製代碼
和 AST 節點同樣它們也有 start,end,loc 屬性。.
語法分析階段會把一個令牌流轉換成 AST 的形式。 這個階段會使用令牌中的信息把它們轉換成一個 AST 的表述結構,這樣更易於後續的操做。
轉換步驟接收 AST 並對其進行遍歷,在此過程當中對節點進行添加、更新及移除等操做。 這是 Babel 或是其餘編譯器中最複雜的過程 同時也是插件將要介入工做的部分,這將是本手冊的主要內容, 所以讓咱們慢慢來。
代碼生成步驟把最終(通過一系列轉換以後)的 AST轉換成字符串形式的代碼,同時建立源碼映射(source maps)。.
代碼生成其實很簡單:深度優先遍歷整個 AST,而後構建能夠表示轉換後代碼的字符串。
想要轉換 AST 你須要進行遞歸的樹形遍歷。
比方說咱們有一個 FunctionDeclaration 類型。它有幾個屬性:id,params,和 body,每個都有一些內嵌節點。
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
params: [{
type: "Identifier",
name: "n"
}],
body: {
type: "BlockStatement",
body: [{
type: "ReturnStatement",
argument: {
type: "BinaryExpression",
operator: "*",
left: {
type: "Identifier",
name: "n"
},
right: {
type: "Identifier",
name: "n"
}
}
}]
}
}複製代碼
因而咱們從 FunctionDeclaration 開始而且咱們知道它的內部屬性(即:id,params,body),因此咱們依次訪問每個屬性及它們的子節點。
接着咱們來到 id,它是一個 Identifier。Identifier 沒有任何子節點屬性,因此咱們繼續。
以後是 params,因爲它是一個數組節點因此咱們訪問其中的每個,它們都是 Identifier 類型的單一節點,而後咱們繼續。
此時咱們來到了 body,這是一個 BlockStatement 而且也有一個 body節點,並且也是一個數組節點,咱們繼續訪問其中的每個。
這裏惟一的一個屬性是 ReturnStatement 節點,它有一個 argument,咱們訪問 argument 就找到了 BinaryExpression。.
BinaryExpression 有一個 operator,一個 left,和一個 right。 Operator 不是一個節點,它只是一個值所以咱們不用繼續向內遍歷,咱們只須要訪問 left 和 right。.
Babel 的轉換步驟全都是這樣的遍歷過程。
當咱們談及「進入」一個節點,其實是說咱們在訪問它們, 之因此使用這樣的術語是由於有一個訪問者模式(visitor)的概念。.
訪問者是一個用於 AST 遍歷的跨語言的模式。 簡單的說它們就是一個對象,定義了用於在一個樹狀結構中獲取具體節點的方法。 這麼說有些抽象因此讓咱們來看一個例子。
const MyVisitor = {
Identifier() {
console.log("Called!");
}
};複製代碼
注意: Identifier() { ... } 是 Identifier: { enter() { ... } } 的簡寫形式。.複製代碼
這是一個簡單的訪問者,把它用於遍歷中時,每當在樹中碰見一個 Identifier 的時候會調用 Identifier() 方法。
因此在下面的代碼中 Identifier() 方法會被調用四次(包括 square 在內,總共有四個 Identifier)。).
function square(n) {
return n * n;
}複製代碼
Called!
Called!
Called!
Called!複製代碼
這些調用都發生在進入節點時,不過有時候咱們也能夠在退出時調用訪問者方法。.
假設咱們有一個樹狀結構:
- FunctionDeclaration
- Identifier (id)
- Identifier (params[0])
- BlockStatement (body)
- ReturnStatement (body)
- BinaryExpression (argument)
- Identifier (left)
- Identifier (right)複製代碼
當咱們向下遍歷這顆樹的每個分支時咱們最終會走到盡頭,因而咱們須要往上遍歷回去從而獲取到下一個節點。 向下遍歷這棵樹咱們進入每一個節點,向上遍歷回去時咱們退出每一個節點。
讓咱們以上面那棵樹爲例子走一遍這個過程。
因此當建立訪問者時你實際上有兩次機會來訪問一個節點。
const MyVisitor = {
Identifier: {
enter() {
console.log("Entered!");
},
exit() {
console.log("Exited!");
}
}
};複製代碼
AST 一般會有許多節點,那麼節點直接如何相互關聯? 咱們能夠用一個巨大的可變對象讓你來操做以及徹底訪問(節點的關係),或者咱們能夠用Paths(路徑)來簡化這件事情。.
Path 是一個對象,它表示兩個節點之間的鏈接。
舉例來講若是咱們有如下的節點和它的子節點:
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
...
}複製代碼
將子節點 Identifier 表示爲路徑的話,看起來是這樣的:
{
"parent": {
"type": "FunctionDeclaration",
"id": {...},
....
},
"node": {
"type": "Identifier",
"name": "square"
}
}複製代碼
同時它還有關於該路徑的附加元數據:
{
"parent": {...},
"node": {...},
"hub": {...},
"contexts": [],
"data": {},
"shouldSkip": false,
"shouldStop": false,
"removed": false,
"state": null,
"opts": null,
"skipKeys": null,
"parentPath": null,
"context": null,
"container": null,
"listKey": null,
"inList": false,
"parentKey": null,
"key": null,
"scope": null,
"type": null,
"typeAnnotation": null
}複製代碼
固然還有成堆的方法,它們和添加、更新、移動和刪除節點有關,不過咱們後面再說。
能夠這麼說,路徑是對於節點在數中的位置以及其餘各類信息的響應式表述。 當你調用一個方法更改了樹的時候,這些信息也會更新。 Babel 幫你管理着這一切從而讓你能更輕鬆的操做節點而且儘可能保證無狀態化。(譯註:意即儘量少的讓你來維護狀態)
當你有一個擁有 Identifier() 方法的訪問者時,你其實是在訪問路徑而不是節點。 如此一來你能夠操做節點的響應式表述(譯註:即路徑)而不是節點自己。
const MyVisitor = {
Identifier(path) {
console.log("Visiting: " + path.node.name);
}
};複製代碼
a + b + c;
Visiting: a
Visiting: b
Visiting: c複製代碼
State(狀態)
狀態是 AST 轉換的敵人。狀態會不停的找你麻煩,你對狀態的預估到最後幾乎老是錯的,由於你沒法預先考慮到全部的語法。
考慮下列代碼:
function square(n) {
return n * n;
}複製代碼
讓咱們寫一個把 n 重命名爲 x 的訪問者的快速實現:.
let paramName;
const MyVisitor = {
FunctionDeclaration(path) {
const param = path.node.params[0];
paramName = param.name;
param.name = "x";
},
Identifier(path) {
if (path.node.name === paramName) {
path.node.name = "x";
}
}
};複製代碼
對上面的代碼來講或許能行,但咱們很容易就能「搞壞」它:
function square(n) {
return n * n;
}
n;複製代碼
更好的處理方式是遞歸。那麼讓咱們來像克里斯托佛·諾蘭的電影那樣來把一個訪問者放進另一個訪問者裏面。
const updateParamNameVisitor = {
Identifier(path) {
if (path.node.name === this.paramName) {
path.node.name = "x";
}
}
};
const MyVisitor = {
FunctionDeclaration(path) {
const param = path.node.params[0];
const paramName = param.name;
param.name = "x";
path.traverse(updateParamNameVisitor, { paramName });
}
};複製代碼
固然,這只是一個刻意捏造的例子,不過它演示瞭如何從訪問者中消除全局狀態。
接下來讓咱們引入做用域(scope)的概念。 JavaScript 擁有詞法做用域,代碼塊建立新的做用域並造成一個樹狀結構。
// global scope
function scopeOne() {
// scope 1
function scopeTwo() {
// scope 2
}
}複製代碼
在 JavaScript 中,每當你建立了一個引用,無論是經過變量(variable)、函數(function)、類型(class)、參數(params)、模塊導入(import)、標籤(label)等等,它都屬於當前做用域。
var global = "I am in the global scope";
function scopeOne() {
var one = "I am in the scope created by `scopeOne()`";
function scopeTwo() {
var two = "I am in the scope created by `scopeTwo()`";
}
}複製代碼
處於深層做用域代碼可使用高(外)層做用域的引用。
function scopeOne() {
var one = "I am in the scope created by `scopeOne()`";
function scopeTwo() {
one = "I am updating the reference in `scopeOne` inside `scopeTwo`";
}
}複製代碼
低(內)層做用域也能夠建立(和外層)同名的引用而無須更改它。
function scopeOne() {
var one = "I am in the scope created by `scopeOne()`";
function scopeTwo() {
var one = "I am creating a new `one` but leaving reference in `scopeOne()` alone.";
}
}複製代碼
當編寫一個轉換器時,咱們需要當心做用域。咱們得確保在改變代碼的各個部分時不會破壞它。
咱們會想要添加新的引用而且保證它們不會和已經存在的引用衝突。 又或者咱們只是想要找出變量在哪裏被引用的。 咱們須要能在給定做用域內跟蹤這些引用。
做用域能夠表述爲:
{
path: path,
block: path.node,
parentBlock: path.parent,
parent: parentScope,
bindings: [...]
}複製代碼
當你建立一個新的做用域時須要給它一個路徑及父級做用域。以後在遍歷過程當中它會在改做用於內收集全部的引用(「綁定」)。
這些作好以後,你將擁有許多用於做用域上的方法。咱們稍後再講這些。
Bindings(綁定)
引用從屬於特定的做用域;這種關係被稱做:綁定(binding)。.
function scopeOnce() {
var ref = "This is a binding";
ref; // This is a reference to a binding
function scopeTwo() {
ref; // This is a reference to a binding from a lower scope
}
}複製代碼
一個綁定看起來以下:
{
identifier: node,
scope: scope,
path: path,
kind: 'var',
referenced: true,
references: 3,
referencePaths: [path, path, path],
constant: false,
constantViolations: [path]
}複製代碼
有了這些信息你就能夠查找一個綁定的全部引用,而且知道綁定的類型是什麼(參數,定義等等),尋找到它所屬的做用域,或者獲得它的標識符的拷貝。 你甚至能夠知道它是不是一個常量,並查看是哪一個路徑讓它不是一個常量。
知道綁定是否爲常量在不少狀況下都會頗有用,最大的用處就是代碼壓縮。
function scopeOne() {
var ref1 = "This is a constant binding";
becauseNothingEverChangesTheValueOf(ref1);
function scopeTwo() {
var ref2 = "This is *not* a constant binding";
ref2 = "Because this changes the value";
}
}複製代碼
Babel 其實是一系列的模塊。本節咱們將探索一些主要的模塊,解釋它們是作什麼的以及如何使用它們。
注意:本節內容不是詳細的 API 文檔的替代品,正式的 API 文檔將很快提供出來。複製代碼
Babylon 是 Babel 的解析器。最初是 Acorn 的一份 fork,它很是快,易於使用,而且針對非標準特性(以及那些將來的標準特性)設計了一個基於插件的架構。
首先,讓咱們先安裝它。
$ npm install --save babylon複製代碼
讓咱們從解析簡單的字符形式代碼開始:
import * as babylon from "babylon";
const code = `function square(n) {
return n * n;
}`;
babylon.parse(code);
// Node {
// type: "File",
// start: 0,
// end: 38,
// loc: SourceLocation {...},
// program: Node {...},
// comments: [],
// tokens: [...]
// }複製代碼
咱們還能傳遞選項給 parse():
babylon.parse(code, {
sourceType: "module", // default: "script"
plugins: ["jsx"] // default: []
});複製代碼
sourceType 能夠是 "module" 或者 "script",它表示 Babylon 應該用哪一種模式來解析。 "module" 將會在嚴格模式下解析而且容許模塊定義,"script" 則不會。
注意: sourceType 的默認值是 "script" 而且在發現 import 或 export 時產生錯誤。 使用 scourceType: "module" 來避免這些錯誤。複製代碼
由於 Babylon 使用了基於插件的架構,所以 plugins 選項能夠開啓內置插件。 注意 Babylon 還沒有對外部插件開放此 API 接口,不過將來會開放的。
能夠在 Babylon README 查看全部插件的列表。.
Babel Tranverse(遍歷)模塊維護了整棵樹的狀態,而且負責替換、移除和添加節點。
運行如下命令安裝:
$ npm install --save babel-traverse複製代碼
咱們能夠配合 Babylon 一塊兒使用來遍歷和更新節點:
import * as babylon from "babylon";
import traverse from "babel-traverse";
const code = `function square(n) {
return n * n;
}`;
const ast = babylon.parse(code);
traverse(ast, {
enter(path) {
if (
path.node.type === "Identifier" &&
path.node.name === "n"
) {
path.node.name = "x";
}
}
});複製代碼
Babel Types(類型)模塊是一個用於 AST 節點的 Lodash 式工具庫。 譯註:Lodash 是一個 JavaScript 函數工具庫,提供了基於函數式編程風格的衆多工具函數)它包含了構造、驗證以及變換 AST 節點的方法。 其設計周到的工具方法有助於編寫清晰簡單的 AST 邏輯。
運行如下命令來安裝它:
$ npm install --save babel-types複製代碼
而後以下所示來使用:
import traverse from "babel-traverse";
import * as t from "babel-types";
traverse(ast, {
enter(path) {
if (t.isIdentifier(path.node, { name: "n" })) {
path.node.name = "x";
}
}
});複製代碼
Babel Types模塊擁有每個單一類型節點的定義,包括有哪些屬性分別屬於哪裏,哪些值是合法的,如何構建該節點,該節點應該如何去遍歷,以及節點的別名等信息。
單一節點類型定義的形式以下:
defineType("BinaryExpression", {
builder: ["operator", "left", "right"],
fields: {
operator: {
validate: assertValueType("string")
},
left: {
validate: assertNodeType("Expression")
},
right: {
validate: assertNodeType("Expression")
}
},
visitor: ["left", "right"],
aliases: ["Binary", "Expression"]
});複製代碼
你會注意到上面的 BinaryExpression 定義有一個 builder 字段。.
builder: ["operator", "left", "right"]複製代碼
這是因爲每個節點類型都有構建器方法:
t.binaryExpression("*", t.identifier("a"), t.identifier("b"));複製代碼
它能夠建立以下所示的 AST:
{
type: "BinaryExpression",
operator: "*",
left: {
type: "Identifier",
name: "a"
},
right: {
type: "Identifier",
name: "b"
}
}複製代碼
當打印出來(輸出)以後是這樣的:
a * b複製代碼
構建器還會驗證自身建立的節點,並在錯誤使用的情形下拋出描述性的錯誤。這就引出了接下來的一種方法。
BinaryExpression 的定義還包含了節點的 fields 字段信息而且指示瞭如何驗證它們。
fields: {
operator: {
validate: assertValueType("string")
},
left: {
validate: assertNodeType("Expression")
},
right: {
validate: assertNodeType("Expression")
}
}複製代碼
這能夠用來建立兩種類型的驗證方法。第一種是 isX。.
t.isBinaryExpression(maybeBinaryExpressionNode);複製代碼
此方法用來確保節點是一個二進制表達式,不過你也能夠傳入第二個參數來確保節點包含特定的屬性和值。
t.isBinaryExpression(maybeBinaryExpressionNode, { operator: "*" });複製代碼
這些方法還有一種更加,嗯,斷言式的版本,會拋出異常而不是返回 true 或 false。.
t.assertBinaryExpression(maybeBinaryExpressionNode);
t.assertBinaryExpression(maybeBinaryExpressionNode, { operator: "*" });
// Error: Expected type "BinaryExpression" with option { "operator": "*" }複製代碼
Babel Generator模塊是 Babel 的代碼生成器。它將 AST 輸出爲代碼幷包括源碼映射(sourcemaps)。
運行如下命令來安裝它:
$ npm install --save babel-generator複製代碼
而後以下所示使用:
import * as babylon from "babylon";
import generate from "babel-generator";
const code = `function square(n) {
return n * n;
}`;
const ast = babylon.parse(code);
generate(ast, null, code);
// {
// code: "...",
// map: "..."
// }複製代碼
你也能夠給 generate() 傳遞選項。.
generate(ast, {
retainLines: false,
compact: "auto",
concise: false,
quotes: "double",
// ...
}, code);複製代碼
Babel Template模塊是一個很小但卻很是有用的模塊。它能讓你編寫帶有佔位符的字符串形式的代碼,你能夠用此來替代大量的手工構建的 AST。
$ npm install --save babel-template
import template from "babel-template";
import generate from "babel-generator";
import * as t from "babel-types";
const buildRequire = template(`
var IMPORT_NAME = require(SOURCE);
`);
const ast = buildRequire({
IMPORT_NAME: t.identifier("myModule"),
SOURCE: t.stringLiteral("my-module")
});
console.log(generate(ast).code);
// var myModule = require("my-module");複製代碼