文章概覽
主要包括:Babel如何進行轉碼、外掛編寫的入門基礎、例項講解如何編寫外掛。
閱讀本文前,需要讀者對Babel外掛如何使用、配置有一定了解,可以參考筆者之前的文章。
本文所有例子可以在 筆者的github 找到,歡迎訪問筆者部落格獲取更多相關文章。
Babel執行階段
首先來了解Babel轉碼的過程分三個階段:分析(parse)、轉換(transform)、生成(generate)。
其中,分析、生成階段由Babel核心完成,而轉換階段,則由Babel外掛完成,這也是本文的重點。
分析
Babel讀入原始碼,經過詞法分析、語法分析後,生成抽象語法樹(AST)。
1 |
parse(sourceCode) => AST |
轉換
經過前一階段的程式碼分析,Babel得到了AST。在原始AST的基礎上,Babel通過外掛,對其進行修改,比如新增、刪除、修改後,得到新的AST。
1 |
transform(AST, BabelPlugins) => newAST |
生成
通過前一階段的轉換,Babel得到了新的AST,然後就可以逆向操作,生成新的程式碼。
1 |
generate(newAST) => newSourceCode |
外掛基礎入門
典型的Babel外掛結構如下所示。需要主要的有:
- babelType:類似lodash那樣的工具集,主要用來操作AST節點,比如建立、校驗、轉變等。舉例:判斷某個節點是不是識別符號(identifier)。
- path:AST中有很多節點,每個節點可能有不同的屬性,並且節點之間可能存在關聯。path是個物件,它代表了兩個節點之間的關聯。你可以在path上訪問到節點的屬性,也可以通過path來訪問到關聯的節點(比如父節點、兄弟節點等)
- state:代表了外掛的狀態,你可以通過state來訪問外掛的配置項。
- visitor:Babel採取遞迴的方式訪問AST的每個節點,之所以叫做visitor,只是因為有個類似的設計模式叫做訪問者模式,不用在意背後的細節。
- Identifier、ASTNodeTypeHere:AST的每個節點,都有對應的節點型別,比如識別符號(Identifier)、函式宣告(FunctionDeclaration)等,可以在visitor上宣告同名的屬性,當Babel遍歷到相應型別的節點,屬性對應的方法就會被呼叫,傳入的引數就是path、state。
1 2 3 4 5 6 7 8 |
export default function({ types: babelTypes }) { return { visitor: { Identifier(path, state) {}, ASTNodeTypeHere(path, state) {} } }; }; |
極簡外掛例項
在本例子中,我們實現一個毫無意義的外掛:將所有名稱為bad的識別符號,轉成good。完整程式碼在這裡。
首先,安裝專案依賴。
1 2 |
npm init -f npm install --save-dev babel-cli |
接著,建立外掛。判斷識別符號的名稱是否是bad,如果是則替換成good。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// plugin.js module.exports = function({ types: babelTypes }) { return { name: "deadly-simple-plugin-example", visitor: { Identifier(path, state) { if (path.node.name === 'bad') { path.node.name = 'good'; } } } }; }; |
原始碼前的原始碼:
1 2 |
// index.js let bad = true; |
執行轉碼命令:
1 |
npx babel --plugins ./plugin.js index.js |
輸出轉碼結果:
1 2 |
// index.js let good = true; |
外掛配置
外掛可以有自己的配置項。我們修改前面的例子,看下在Babel外掛中如何獲取配置項。完整程式碼在這裡
首先,我們新建 .babelrc,傳入配置項。
1 2 3 4 5 6 |
{ "plugins": [ ["./plugin", { "bad": "good", "dead": "alive" }] ] } |
然後,修改外掛程式碼。我們從 state.opts 中獲取到配置引數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// plugin.js module.exports = function({ types: babelTypes }) { return { name: "deadly-simple-plugin-example", visitor: { Identifier(path, state) { let name = path.node.name; if (state.opts[name]) { path.node.name = state.opts[name]; } } } }; }; |
修改需要轉換的程式碼:
1 2 3 |
// index.js let bad = true; let dead = true; |
執行轉碼命令 npx babel index.js
,轉碼結果如下:
1 2 3 |
// index.js let good = true; let alive = true; |
複雜外掛例子:替換process.env.NODE_ENV
下面,來看一個稍微複雜一點但比較實用的例子:替換 process.env.NODE_ENV。示例完整程式碼可以在 這裡找到,參考了這個外掛。
在很多開源專案中,我們經常會看到類似下面的程式碼,對這些程式碼,需要在構建階段進行處理,比如進行替換。
1 2 3 4 |
// index.js if ( process.env.NODE_ENV === 'development' ) { console.log('我是程式猿小卡'); } |
下面,我們建立一個叫做 node-env-replacer 的外掛,程式碼如下,下面會對外掛程式碼進行講解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// plugin.js module.exports = function({ types: babelTypes }) { return { name: "node-env-replacer", visitor: { // 成員表示式 MemberExpression(path, state) { // 如果 object 對應的節點匹配了模式 "process.env" if (path.get("object").matchesPattern("process.env")) { // 這裡返回結果為字串字面量型別的節點 const key = path.toComputedKey(); if ( babelTypes.isStringLiteral(key) ) { // path.replaceWith( newNode ) 用來替換當前節點 // babelTypes.valueToNode( value ) 用來建立節點,如果value是字串,則返回字串字面量型別的節點 path.replaceWith(babelTypes.valueToNode(process.env[key.value])); } } } } }; }; |
外掛程式碼講解
這次我們處理的是成員表達方式(MemberExpression)。對於MemberExpression,BabelType的定義如下:
MemberExpression 主要是由 object、property、computed、optional 組成的。對於本例子來說,object 是 process.env 對應的節點,property 為 NODE_ENV 對應的節點。
1 2 3 4 5 |
defineType("MemberExpression", { builder: ["object", "property", "computed", "optional"], visitor: ["object", "property"], // ... }); |
前面提到,path對應了節點的屬性,以及節點的關聯關係。path.get(“object”) 獲取到的就是 object(process.env)對應的 path例項。
matchesPattern(pattern) 檢查某個節點是否符合某種模式(pattern)。本例子中,path.get(“object”).matchesPattern(“process.env”) 檢查 object 是否符合 “process.env” 這種模式。比如 成員表示式 process.env.NODE_ENV 為true,而成員表示式 process.hello.NODE_ENV 返回false。
1 |
if (path.get("object").matchesPattern("process.env")) { } |
接著,通過 path.toComputedKey() 獲取成員表示式的鍵(key),對於對於MemberExpression,返回的是型別為字串字面量(stringLiteral)的節點。
1 |
const key = path.toComputedKey(); |
if ( babelTypes.isStringLiteral(key) ) 判斷 key 是否為字串字面量,如果是,則返回true。
path.replaceWith( node ) 方法用來替換節點。babelTypes.valueToNode( value ) 用來建立節點,如果value是字串,則返回字串字面量型別的節點。
1 |
path.replaceWith(babelTypes.valueToNode(process.env[key.value])); |
執行外掛
命令如下:
1 |
npx babel --plugins ./plugin.js index.js |
轉換結果:
1 2 3 4 |
// index.js if ('development' === 'development') { console.log('我是程式猿小卡'); } |
小結
Babel的外掛入門比較簡單,照葫蘆畫瓢即可。在編寫外掛過程中,可能會遇到的主要障礙,包括對ECMA規範不瞭解、對Babel的API不瞭解。
- 對ECMA規範不瞭解:MemberExpression、FunctionDeclaration、Identifier等都是規範裡的術語,如果對規範沒有一定的瞭解,轉換程式碼的時候就不知道如何入手。建議讀者稍微瞭解下ECMA規範。
- 對Babel的API不瞭解:Babel相關API的文件比較少,這會對外掛編寫造成不小的困難,目前比較好的解決辦法,就是參考現有的外掛進行修改。
總而言之,就是多看多寫多查。
這裡再留個小問題,前面外掛替換了 process.env.NODE_ENV,如果是下面程式碼該怎麼替換?
1 |
process.env['NODE_' + 'ENV']; |