Babel插件開發入門指南


文章概覽

主要包括:Babel如何進行轉碼、插件編寫的入門基礎、實例講解如何編寫插件。

閱讀本文前,需要讀者對Babel插件如何使用、配置有一定了解,可以參考筆者之前的文章

本文所有例子可以在 筆者的github 找到,歡迎訪問筆者博客獲取更多相關文章。

Babel運行階段

首先來了解Babel轉碼的過程分三個階段:分析(parse)、轉換(transform)、生成(generate)。

其中,分析、生成階段由Babel核心完成,而轉換階段,則由Babel插件完成,這也是本文的重點。

分析

Babel讀入源代碼,經過詞法分析、語法分析后,生成抽象語法樹(AST)

parse(sourceCode) => AST

轉換

經過前一階段的代碼分析,Babel得到了AST。在原始AST的基礎上,Babel通過插件,對其進行修改,比如新增、刪除、修改后,得到新的AST。

transform(AST, BabelPlugins) => newAST

生成

通過前一階段的轉換,Babel得到了新的AST,然后就可以逆向操作,生成新的代碼。

generate(newAST) => newSourceCode

插件基礎入門

典型的Babel插件結構,如下代碼所示。

export default function({ types: babelTypes }) {
  return {
    visitor: {
      Identifier(path, state) {},
      ASTNodeTypeHere(path, state) {}
    }
  };
};

需要關注的內容如下:

  • 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不了解。

  1. 對ECMA規范不了解:MemberExpression、FunctionDeclaration、Identifier等都是規范里的術語,如果對規范沒有一定的了解,轉換代碼的時候就不知道如何入手。建議讀者稍微了解下ECMA規范。
  2. 對Babel的API不了解:Babel相關API的文檔比較少,這會對插件編寫造成不小的困難,目前比較好的解決辦法,就是參考現有的插件進行修改。

總而言之,就是多看多寫多查。

這里再留個小問題,前面插件替換了 process.env.NODE_ENV,如果是下面代碼該怎么替換?

process.env['NODE_' + 'ENV'];

相關鏈接

babel-handbook

ECMA-262/5.1

Babel核心package的文檔

熱心開發者貢獻的文檔


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM