Babel,是一個 JavaScript 的編譯工具,它能夠將 es6+語法的代碼,轉換爲瀏覽器兼容的低版本的代碼。它簡直就是一個神兵利器,前端工程師擁有了它,就能夠在項目中使用一些較新的 es 語法。筆者決定弄懂它,並實現一個本身的 Babel 插件。javascript
Babel 的工做原理,能夠用以下公式表述。它實際上就是接受輸入的源代碼,而後對它作一些處理和轉換,最後輸出爲目標版本的代碼。前端
const babel = sourceCode => distCode
複製代碼
在將輸入的源碼作處理或者轉換時,這就須要用到了它的插件系統。一個插件只負責處理一件事,好比@babel/plugin-transform-arrow-functions
,就是負責將箭頭函數轉換爲普通函數的插件。Babel 提供了很是多的插件,這樣就足以保證能夠將新的 es 語法轉換爲舊版本的代碼形式。想了解詳細的 Babel 插件系統,能夠查看Babel#Plugins。java
若是不配置任何的插件,Babel 將不會對源碼作任何的處理,它只會照原樣輸出。下面舉個例子,node
const babel = require("@babel/core")
const code = ` const a = () => { console.log(1); } `
// 沒有配置任何plugin,那麼轉換以後的code將沒有任何變化
babel.transform(code, undefined, (err, result) => {
if (err) {
throw err
}
console.log(result.code)
})
複製代碼
咱們將箭頭函數使用 Babel 來轉換,可是沒有配置任何的插件,最後轉換以後的結果將和輸入的代碼一摸同樣。git
➜ babel node scripts/index.ts
const a = () => {
console.log(1);
};
複製代碼
若是配置了@babel/plugin-transform-arrow-functions
,Babel 就能正常將咱們的箭頭函數轉換爲普通函數的形式了。以下,es6
// 配置@babel/plugin-transform-arrow-functions
babel.transform(
code,
{ plugins: ["@babel/plugin-transform-arrow-functions"] },
(err, result) => {
if (err) {
throw err
}
console.log(result.code)
}
)
複製代碼
轉換以後的代碼以下,github
➜ babel node scripts/index.ts
const a = function () {
console.log(1);
};
複製代碼
對於其餘的語法形式的轉換,能夠添加其餘的插件。若是僅僅這樣,對於一個實際項目代碼的轉換,將要配置很是多的插件。爲了簡化這種形式,Babel 又提供了 Presets,簡單的說,就是將不少個插件集合從新命名爲一個新名稱。這樣,只須要配置了這個 Presets,那麼就相對於配置它所包含的全部的插件。Babel 定義了經常使用的 Presets,詳細能夠查看Babel#Presets。express
經過加入插件處理的方式,Babel 將會有很是好的可擴展性和可插拔性,好比 esNext 中又添加了一個新的語法糖,那麼 Babel 只須要單獨提供這個新語法處理的插件,並將它配置進去就能夠了。對於前端工程師們,也能夠根據實際業務需求,寫本身的插件,將輸入的源碼處理成本身想要的輸出。api
爲了能寫出本身的 Babel 插件,咱們就須要知道 Babel 將輸入的代碼轉換成什麼樣子,插件接受的參數又是什麼樣子,最後須要返回的值是什麼樣子。數組
Babel 會將輸入的源代碼先轉換成 AST(Abstract Syntax Tree),而後將 AST 做爲參數傳給插件,插件將在 AST 上作處理,能夠添加,刪除或者改變節點。例如,咱們上面例子中箭頭函數 a,生成的 AST 大體結構以下,
"program": {
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "ArrowFunctionExpression",
"params": [],
"body": {
"type": "BlockStatement",
"body": [
{ ↔ }
],
}
}
}
],
"kind": "const"
}
],
},
複製代碼
AST 能夠當作一棵樹,它包含了不少的節點,每一個節點都會包含一個 type 字段,這個 type 字段就是用來代表當前節點的類型,好比上面的Identifier
代表是標識符,ArrowFunctionExpression
代表是箭頭函數表達式。想詳細瞭解 AST 結構,能夠查看astexplorer。
要處理 AST 樹,就得遍歷這顆樹,找到咱們要處理的節點位置。對於一棵樹的遍歷,有 DFS(深度優先搜索)和 BFS(廣度優先搜索)兩種方式。對於 AST 的遍歷,使用的是 DFS 方式。Babel 提供了@babel/traverse
來遍歷它,能夠很方便的找到須要處理的節點位置。例如上面的例子,咱們能夠像下面這樣找到console.log(1)
中1
這個節點位置,
const babel = require("@babel/core")
const traverse = require("@babel/traverse")
const code = ` const a = () => { console.log(1); } `
babel.parse(code, null, (err, ast) => {
if (err) {
throw err
}
traverse(ast, {
NumericLiteral(path) {
console.log(JSON.stringify(path.node, null, 4))
},
})
})
複製代碼
因爲console.log(1)
接受的參數是一個數字字面量,因此它對應的type
就是NumericLiteral
。最後找到這個節點的信息以下,
➜ babel node scripts/index.ts
{
"type": "NumericLiteral",
"start": 37,
"end": 38,
"loc": {
"start": {
"line": 3,
"column": 16
},
"end": {
"line": 3,
"column": 17
}
},
"extra": {
"rawValue": 1,
"raw": "1"
},
"value": 1
}
複製代碼
能夠看到,節點包含了 type,loc 信息,以及 value 等信息。更多關於 traverse 的使用,能夠查看這裏Babel#traverse。
根據上面的思路,咱們能夠得出以下結論,
Babel 中插件接受 AST 做爲參數,而後能夠在 AST 上作一些自定義的處理,最後返回處理以後的 AST。
爲了驗證這個結論正確性,咱們來看看官方的@babel/plugin-transform-arrow-functions
的源碼,源碼只有 28 行代碼,我貼出來,並作一些本身的註釋,一塊兒看看。
import { declare } from "@babel/helper-plugin-utils";
import type NodePath from "@babel/traverse";
export default function declare((api, options) {
// 判斷當前Babel版本是不是v7.x
api.assertVersion(7);
// 接受咱們傳入的參數
const { spec } = options;
// 返回一個對象
return {
name: "transform-arrow-functions",
visitor: {
ArrowFunctionExpression(
path: NodePath<BabelNodeArrowFunctionExpression>,
) {
// 先判斷是否是箭頭函數表達式,不是就直接返回
if (!path.isArrowFunctionExpression()) return;
// 將箭頭函數轉爲函數表達式
path.arrowFunctionToExpression({
allowInsertArrow: false,
specCompliant: !!spec,
});
},
},
};
});
複製代碼
從源碼能夠看出,它返回一個declare
函數。這個函數接受兩個參數,一個api
,一個是options
。函數處理步驟以下,
name
和visitor
;其中,visitor
又是一個對象,它才真正包含對肩頭函數表達式的處理。實際上,path.arrowFunctionToExpression
就是使用@babel/types
中arrowfunctionexpression
,詳細能夠查看babel-types#arrowfunctionexpression。
跟咱們猜測的 Babel 插件樣子有點出入,可是它包含了咱們猜測的內容。最後,咱們能夠總結出寫一個 Babel 插件的樣子應該是這樣的,
export default function declare(api, options) {
// api能夠作一些版本兼容性判斷,或者緩存相關的。
// options就是咱們配置插件時,傳入的參數,這裏插件內部就能夠使用了
return {
name: "my-custorm-plugin",
visitor: {
// 遍歷AST作處理
},
}
}
複製代碼
清楚了 Babel 插件的模版形式,就能夠按照這個模版寫咱們自定義的功能插件。假設,咱們要寫的一個 Babel 插件,就是去掉全部的console.log
相關調試信息的代碼。
// 源代碼
const a = () => {
console.log(1)
}
複製代碼
例如上面的代碼通過咱們的 Babel 插件處理以後,輸出的代碼應該是一個空的箭頭函數 a,
// 轉換以後
const a = () => {}
複製代碼
根據 Babel 插件模版代碼,咱們能夠這樣實現以下,
// plugins/remove-console-log.js
const types = require("@babel/types")
module.exports = function declare(api, options) {
api.assertVersion(7)
return {
name: "remove-console-log",
visitor: {
ExpressionStatement(path) {
const expression = path.node.expression
if (types.isCallExpression(expression)) {
const callee = expression.callee
if (types.isMemberExpression(callee)) {
const objName = callee.object.name
const methodName = callee.property.name
if (objName === "console" && methodName === "log") {
path.remove()
}
}
}
},
},
}
}
複製代碼
而後在 babel.config.js 中配置以下,
module.exports = {
plugins: ["./plugins/remove-console-log.js"],
}
複製代碼
最後,咱們經過 Babel 轉換以後就能夠獲得咱們指望的結果了。
經過本身實現一個 Babel 插件,而後貫穿整個過程把 Babel 原理弄清楚。上面其實還有一個小知識點,就是 Babel 怎麼將源碼轉換成 AST 的。其實,它的過程也不難理解,只是在轉換爲 AST 以前,須要先進行詞法分析,把源碼字符串轉換成 Token 數組;而後根據詞法分析獲得的結果,轉換成 AST。完整的 Babel 原理過程能夠簡單的表述爲以下,
let tokens = tokenizer(input) // 詞法分析
let ast = parser(tokens) // 轉換爲AST
let newAst = transformer(ast) // 調用插件,進行轉換
let output = codeGenerator(newAst) // 最後,生成新的目標代碼
複製代碼
若是想更加詳細研究 Babel 的過程,能夠看看這個簡易的編譯器the-super-tiny-compiler,它實現了完整的流程過程,代碼也很是簡單易懂。