新的一年babel瞭解一下
Babel
是一個 JavaScript
編譯器
很多瀏覽器目前還不支援ES6的程式碼, Babel
的作用就是把瀏覽器不資辭的程式碼編譯成資辭的程式碼。
注意很重要的一點就是, Babel
只是轉譯新標準引入的語法,比如ES6的箭頭函式轉譯成ES5的函式, 但是對於新標準引入的新的原生物件,部分原生物件新增的原型方法,新增的API等(如 Set
、 Promise
),這些 Babel
是不會轉譯的,需要引入 polyfill
來解決。
API
Babel
實際上是一組模組的集合。
- babylon
Babel
的解析器
首先,安裝一下這個外掛。
npm i babylon -S 複製程式碼
先從解析一個程式碼字串開始:
import * as babylon from 'babylon'; const code = `function add(m, n) { return m + n; }`; babylon.parse(code); // Node { //type: "File", //start: 0, //end: 38, //loc: SourceLocation {...}, //program: Node {...}, //comments: [], //tokens: [...] // } 複製程式碼
用於對 AST 的遍歷,維護了整棵樹的狀態,並且負責替換、移除和新增節點。
執行以下命令安裝:
npm i babel-traverse -S 複製程式碼
import * as babylon from 'babylon'; import traverse from 'babel-traverse'; const code = `function add(m, n) { return m + n; }`; const ast = babylon.parse(code); traverse(ast, { enter(path) { if ( path.node.type === 'Identifier' && path.node.name === 'm' ) { // do something } } }); 複製程式碼
- babel-types 用於 AST 節點的 Lodash 式工具庫, 它包含了構造、驗證以及變換 AST 節點的方法,對編寫處理 AST 邏輯非常有用。
npm i babel-types -S 複製程式碼
import traverse from 'babel-traverse'; import * as t from 'babel-types'; traverse(ast, { enter(path) { if (t.isIdentifier(path.node, { name: 'm' })) { // do something } } }); 複製程式碼
Babel 的程式碼生成器,它讀取AST並將其轉換為程式碼和原始碼對映(sourcemaps)
npm i babel-generator -S 複製程式碼
import * as babylon from 'babylon'; import generate from 'babel-generator'; const code = `function add(m, n) { return m + n; }`; const ast = babylon.parse(code); generate(ast, {}, code); // { //code: "...", //map: "...", //rawMappings: "..." // } 複製程式碼
Babel
是怎麼工作的

為了理解 Babel
,我們從ES6最受歡迎的特性箭頭函式入手。
假設要把下面這個箭頭函式的 Javascript
程式碼
(foo, bar) => foo + bar; 複製程式碼
編譯成瀏覽器支援的程式碼:
'use strict'; (function (foo, bar) { return foo + bar; }); 複製程式碼
Babel的編譯過程和大多數其他語言的編譯器相似,可以分為三個階段:
- 解析(Parsing):將程式碼字串解析成抽象語法樹。
- 轉換(Transformation):對抽象語法樹進行轉換操作。
- 生成(Code Generation): 根據變換後的抽象語法樹再生成程式碼字串。
解析(Parsing)
Babel
拿到原始碼會把程式碼抽象出來,變成 AST
(抽象語法樹),洋文是 Abstract Syntax Tree
。
抽象語法樹是原始碼的抽象語法結構的樹狀表示,樹上的每個節點都表示原始碼中的一種結構,這所以說是抽象的,是因為抽象語法樹並不會表示出真實語法出現的每一個細節,比如說,巢狀括號被隱含在樹的結構中,並沒有以節點的形式呈現。它們主要用於原始碼的簡單轉換。
箭頭函式 (foo, bar) => foo + bar;
的AST長這樣:
{ "type": "Program", "start": 0, "end": 202, "body": [ { "type": "ExpressionStatement", "start": 179, "end": 202, "expression": { "type": "ArrowFunctionExpression", "start": 179, "end": 202, "id": null, "expression": true, "generator": false, "params": [ { "type": "Identifier", "start": 180, "end": 183, "name": "foo" }, { "type": "Identifier", "start": 185, "end": 188, "name": "bar" } ], "body": { "type": "BinaryExpression", "start": 193, "end": 202, "left": { "type": "Identifier", "start": 193, "end": 196, "name": "foo" }, "operator": "+", "right": { "type": "Identifier", "start": 199, "end": 202, "name": "bar" } } } } ], "sourceType": "module" } 複製程式碼
上面的 AST
描述了原始碼的每個部分以及它們之間的關係,可以自己在這裡試一下astexplorer。
AST
是怎麼來的?解析過程分為兩個步驟:
- 分詞:將整個程式碼字串分割成語法單元陣列
Javascript
程式碼中的語法單元主要指如識別符號(if/else、return、function)、運算子、括號、數字、字串、空格等等能被解析的最小單元
[ { "type": "Punctuator", "value": "(" }, { "type": "Identifier", "value": "foo" }, { "type": "Punctuator", "value": "," }, { "type": "Identifier", "value": "bar" }, { "type": "Punctuator", "value": ")" }, { "type": "Punctuator", "value": "=>" }, { "type": "Identifier", "value": "foo" }, { "type": "Punctuator", "value": "+" }, { "type": "Identifier", "value": "bar" } ] 複製程式碼
- 語法分析:建立分析語法單元之間的關係
語義分析則是將得到的詞彙進行一個立體的組合,確定詞語之間的關係。考慮到程式語言的各種從屬關係的複雜性,語義分析的過程又是在遍歷得到的語法單元組,相對而言就會變得更復雜。
簡單來說語義分析既是對語句和表示式識別,這是個遞迴過程,在解析中, Babel
會在解析每個語句和表示式的過程中設定一個暫存器,用來暫存當前讀取到的語法單元,如果解析失敗,就會返回之前的暫存點,再按照另一種方式進行解析,如果解析成功,則將暫存點銷燬,不斷重複以上操作,直到最後生成對應的語法樹。
轉換(Transformation)
Plugins
外掛應用於 babel
的轉譯過程,尤其是第二個階段 Transformation
,如果這個階段不使用任何外掛,那麼 babel
會原樣輸出程式碼。
Presets
babel
官方幫我們做了一些預設的外掛集,稱之為 preset
,這樣我們只需要使用對應的preset就可以了。每年每個 preset
只編譯當年批准的內容。 而 babel-preset-env
相當於 es2015 ,es2016 ,es2017 及最新版本。
Plugin/Preset 路徑
如果 plugin 是通過 npm 安裝,可以傳入 plugin 名字給 babel,babel 將檢查它是否安裝在 node_modules
中
"plugins": ["babel-plugin-myPlugin"] 複製程式碼
也可以指定你的 plugin/preset 的相對或絕對路徑。
"plugins": ["./node_modules/asdf/plugin"] 複製程式碼
Plugin/Preset 排序
如果兩次轉譯都訪問相同的節點,則轉譯將按照 plugin 或 preset 的規則進行排序然後執行。
- Plugin 會執行在 Preset 之前。
- Plugin 會從第一個開始順序執行。
- Preset 的順序則剛好相反(從最後一個逆序執行)。
例如:
{ "plugins": [ "transform-decorators-legacy", "transform-class-properties" ] } 複製程式碼
將先執行 transform-decorators-legacy
再執行 transform-class-properties
但 preset 是反向的
{ "presets": [ "es2015", "react", "stage-2" ] } 複製程式碼
會按以下順序執行: stage-2
, react
, 最後 es2015
。
生成(Code Generation)
用 babel-generator
通過 AST 樹生成 ES5 程式碼
編寫一個 Babel
外掛
基礎的東西講了些,下面說下具體如何寫外掛。
外掛格式
先從一個接收了當前babel物件作為引數的 function
開始。
export default function(babel) { // plugin contents } 複製程式碼
我們經常會這樣寫
export default function({ types: t }) { // } 複製程式碼
接著返回一個物件,其 visitor
屬性是這個外掛的主要訪問者。
export default function({ types: t }) { return { visitor: { // visitor contents } }; }; 複製程式碼
visitor
中的每個函式接收2個引數: path
和 state
export default function({ types: t }) { return { visitor: { CallExpression(path, state) {} } }; }; 複製程式碼
- 寫一個簡單的外掛
我們寫一個簡單的外掛,把所有定義變數名為 a
的換成 b
, 先從astexplorer看下 var a = 1
的 AST
{ "type": "Program", "start": 0, "end": 10, "body": [ { "type": "VariableDeclaration", "start": 0, "end": 9, "declarations": [ { "type": "VariableDeclarator", "start": 4, "end": 9, "id": { "type": "Identifier", "start": 4, "end": 5, "name": "a" }, "init": { "type": "Literal", "start": 8, "end": 9, "value": 1, "raw": "1" } } ], "kind": "var" } ], "sourceType": "module" } 複製程式碼
從這裡看,要找的節點型別就是 VariableDeclarator
,下面開搞
export default function({ types: t }) { return { visitor: { VariableDeclarator(path, state) { if (path.node.id.name == 'a') { path.node.id = t.identifier('b') } } } } } 複製程式碼
我們要把 id
屬性是 a 的替換成 b 就好了。但是這裡不能直接 path.node.id.name = 'b'
。如果操作的是object,就沒問題,但是這裡是 AST 語法樹,所以想改變某個值,就是用對應的 AST 來替換,現在我們用新的識別符號來替換這個屬性。
測試一下
import * as babel from '@babel/core'; const c = `var a = 1`; const { code } = babel.transform(c, { plugins: [ function({ types: t }) { return { visitor: { VariableDeclarator(path, state) { if (path.node.id.name == 'a') { path.node.id = t.identifier('b') } } } } } ] }) console.log(code); // var b = 1 複製程式碼
- 實現一個簡單的按需打包功能
例如我們要實現把 import { Button } from 'antd'
轉成 import Button from 'antd/lib/button'
通過對比 AST 發現, specifiers
裡的 type
和 source
不同。
// import { Button } from 'antd' "specifiers": [ { "type": "ImportSpecifier", ... } ] 複製程式碼
// import Button from 'antd/lib/button' "specifiers": [ { "type": "ImportDefaultSpecifier", ... } ] 複製程式碼
import * as babel from '@babel/core'; const c = `import { Button } from 'antd'`; const { code } = babel.transform(c, { plugins: [ function({ types: t }) { return { visitor: { ImportDeclaration(path) { const { node: { specifiers, source } } = path; if (!t.isImportDefaultSpecifier(specifiers[0])) { // 對 specifiers 進行判斷 const newImport = specifiers.map(specifier => ( t.importDeclaration( [t.ImportDefaultSpecifier(specifier.local)], t.stringLiteral(`${source.value}/lib/${specifier.local.name}`) ) )) path.replaceWithMultiple(newImport) } } } } } ] }) console.log(code); // import Button from "antd/lib/Button"; 複製程式碼
總結
主要介紹了一下幾個 babel
的 API,和 babel
編譯程式碼的過程以及簡單編寫了一個 babel
外掛