文章概覽
主要包括:Babel如何進行轉碼、插件編寫的入門基礎、實例講解如何編寫插件。javascript
閱讀本文前,須要讀者對Babel插件如何使用、配置有必定了解,能夠參考筆者以前的文章。html
本文全部例子能夠在 筆者的github 找到,歡迎訪問筆者博客獲取更多相關文章。java
Babel運行階段
首先來了解Babel轉碼的過程分三個階段:分析(parse)、轉換(transform)、生成(generate)。node
其中,分析、生成階段由Babel核心完成,而轉換階段,則由Babel插件完成,這也是本文的重點。git
分析
Babel讀入源代碼,通過詞法分析、語法分析後,生成抽象語法樹(AST)。github
parse(sourceCode) => AST
轉換
通過前一階段的代碼分析,Babel獲得了AST。在原始AST的基礎上,Babel經過插件,對其進行修改,好比新增、刪除、修改後,獲得新的AST。npm
transform(AST, BabelPlugins) => newAST
生成
經過前一階段的轉換,Babel獲得了新的AST,而後就能夠逆向操做,生成新的代碼。json
generate(newAST) => newSourceCode
插件基礎入門
典型的Babel插件結構,以下代碼所示。設計模式
export default function({ types: babelTypes }) { return { visitor: { Identifier(path, state) {}, ASTNodeTypeHere(path, state) {} } }; };
須要關注的內容以下:bash
- babelType:相似lodash那樣的工具集,主要用來操做AST節點,好比建立、校驗、轉變等。舉例:判斷某個節點是否是標識符(identifier)。
- path:AST中有不少節點,每一個節點可能有不一樣的屬性,而且節點之間可能存在關聯。path是個對象,它表明了兩個節點之間的關聯。你能夠在path上訪問到節點的屬性,也能夠經過path來訪問到關聯的節點(好比父節點、兄弟節點等)
- state:表明了插件的狀態,你能夠經過state來訪問插件的配置項。
- visitor:Babel採起遞歸的方式訪問AST的每一個節點,之因此叫作visitor,只是由於有個相似的設計模式叫作訪問者模式,不用在乎背後的細節。
- Identifier、ASTNodeTypeHere:AST的每一個節點,都有對應的節點類型,好比標識符(Identifier)、函數聲明(FunctionDeclaration)等,能夠在visitor上聲明同名的屬性,當Babel遍歷到相應類型的節點,屬性對應的方法就會被調用,傳入的參數就是path、state。
極簡插件實例
在本例子中,咱們實現一個毫無心義的插件:將全部名稱爲bad的標識符,轉成good。完整代碼在這裏。
首先,安裝項目依賴。
npm init -f npm install --save-dev babel-cli
接着,建立插件。判斷標識符的名稱是不是bad,若是是則替換成good。
// 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'; } } } }; };
源碼前的源代碼:
// index.js let bad = true;
運行轉碼命令:
npx babel --plugins ./plugin.js index.js
輸出轉碼結果:
// index.js let good = true;
插件配置
插件能夠有本身的配置項。咱們修改前面的例子,看下在Babel插件中如何獲取配置項。完整代碼在這裏
首先,咱們新建 .babelrc,傳入配置項。
{ "plugins": [ ["./plugin", { "bad": "good", "dead": "alive" }] ] }
而後,修改插件代碼。咱們從 state.opts 中獲取到配置參數。
// 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]; } } } }; };
修改須要轉換的代碼:
// index.js let bad = true; let dead = true;
運行轉碼命令 npx babel index.js
,轉碼結果以下:
// index.js let good = true; let alive = true;
複雜插件例子:替換process.env.NODE_ENV
下面,來看一個稍微複雜一點但比較實用的例子:替換 process.env.NODE_ENV。示例完整代碼能夠在 這裏找到,參考了這個插件。
在不少開源項目中,咱們常常會看到相似下面的代碼,對這些代碼,須要在構建階段進行處理,好比進行替換。
// index.js if ( process.env.NODE_ENV === 'development' ) { console.log('我是程序猿小卡'); }
下面,咱們建立一個叫作 node-env-replacer 的插件,代碼以下,下面會對插件代碼進行講解。
// 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 對應的節點。
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。
if (path.get("object").matchesPattern("process.env")) { }
接着,經過 path.toComputedKey() 獲取成員表達式的鍵(key),對於對於MemberExpression,返回的是類型爲字符串字面量(stringLiteral)的節點。
const key = path.toComputedKey();
if ( babelTypes.isStringLiteral(key) ) 判斷 key 是否爲字符串字面量,若是是,則返回true。
path.replaceWith( node ) 方法用來替換節點。babelTypes.valueToNode( value ) 用來建立節點,若是value是字符串,則返回字符串字面量類型的節點。
path.replaceWith(babelTypes.valueToNode(process.env[key.value]));
運行插件
命令以下:
npx babel --plugins ./plugin.js index.js
轉換結果:
// index.js if ('development' === 'development') { console.log('我是程序猿小卡'); }
小結
Babel的插件入門比較簡單,照葫蘆畫瓢便可。在編寫插件過程當中,可能會遇到的主要障礙,包括對ECMA規範不瞭解、對Babel的API不瞭解。
- 對ECMA規範不瞭解:MemberExpression、FunctionDeclaration、Identifier等都是規範裏的術語,若是對規範沒有必定的瞭解,轉換代碼的時候就不知道如何入手。建議讀者稍微瞭解下ECMA規範。
- 對Babel的API不瞭解:Babel相關API的文檔比較少,這會對插件編寫形成不小的困難,目前比較好的解決辦法,就是參考現有的插件進行修改。
總而言之,就是多看多寫多查。
這裏再留個小問題,前面插件替換了 process.env.NODE_ENV,若是是下面代碼該怎麼替換?
process.env['NODE_' + 'ENV'];