從0到1完成一個Babel外掛

skinner發表於2019-04-23

前言

社群裡面有很多關於Babel的文章,有些寫的很好,我自己也受這些文章啟發很大。但我發現一個問題就是,這類文章一進來就講了很多babel底層的概念,說實話對基礎不深的一些童鞋來說,看完之後理解起來還是有一定難度的,最重要的是看完了之後,自己並不知道如何去寫一個Babel外掛,因而這促使了如何從0到1完成一個babel外掛這篇文章的編寫,學習完本篇文章,期望是大家能對Babel有一個整體的認識,知道Babel是什麼?Babel是如何運作的?並且自己能實現一個簡單的Babel外掛。

什麼是Babel

Babel是一個JavaScript編譯器,意思就是說你為Babel提供一些程式碼,Babel做一些轉換,給你返回一些新的程式碼。比如,我們常見的將ES5+的程式碼轉換成ES5+之前的一些程式碼。

Babel的處理步驟

從0到1完成一個Babel外掛

如圖,Babel經過3個處理步驟,分別為解析(parse)轉換(transform)生成(generate)

解析

解析又經過詞法分析語法分析兩個步驟,將輸入的程式碼生成抽象語法數(AST),AST可以理解為就是描述一段程式碼的節點樹,看如下這個例子:

我們輸入

const a = 1
複製程式碼

經過解析(parse),生成如下結構的節點樹(為了方便觀看,去掉了一些表明節點位置資訊的屬性),詳細的可以通過這個工具檢視

{
	"type": "VariableDeclaration",
	"declarations": [
		{
			"type": "VariableDeclarator",
			"id": {
				"type": "Identifier",
				"name": "a"
			},
			"init": {
				"type": "Literal",
				"value": 1,
				"rawValue": 1,
				"raw": "1"
			}
		}
	],
	"kind": "const"
}

複製程式碼

每一個{"type":""}包裹的內容都可以視為一個節點(Node)

轉換

得到了AST抽象語法樹,本質就是一個用來描述程式碼的節點樹(Node),我們就可以通過 樹形遍歷來遍歷它,從而進行程式碼轉換(對節點新增、更新及移除等操作),也就是Babel外掛真正處理的地方

生成

經過轉換之後的AST還是AST,所以我們還需要將AST生成字串形式的程式碼

實戰

Babel的基礎知識還有很多,我覺得一開始瞭解這麼多就夠了,我們現在開始開發一個簡單的Babel轉換。

如前面所說Babel的3個步驟,解析轉換生成,Babel都提供了對應的方法,分別如下:

  • @babel/parser 提供解析parse
  • @babel/traverse 提供轉換traverse
  • @babel/generator 提供生成generate

我們要實現一個外掛,將整個引入元件的程式碼

import { Select as MySelect, Pagination } from 'UI';
// import UI2 from 'xxx-ui';
import * as UI from 'xxx-ui';
複製程式碼

處理為如下按需處理的形式

import MySelect from "/MySelect/MySelect.js";
import Pagination from "/Pagination/Pagination.js"; // import UI2 from 'xxx-ui';
import * as UI from 'xxx-ui';
複製程式碼

第一:搭建一個開發環境

這裡我使用了codesandbox線上編寫的方式,訪問這裡,將需要的依賴包引進來。

const parse = require('@babel/parser').parse;
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
複製程式碼

其中,@babel/types是用於 AST 節點的 Lodash 式工具庫, 它包含了構造、驗證以及變換 AST 節點的方法。 該工具庫包含考慮周到的工具方法,對編寫處理AST邏輯非常有用。

第二:解析程式碼

const code = `import { Select as MySelect, Pagination } from '';
// import UI2 from 'xxx-ui';
import * as UI from 'xxx-ui';
`;
const ast = parse(code);
複製程式碼

第三:轉換程式碼

這步很關鍵,我們的轉換處理都在這一步

traverse(ast, {
  ImportDeclaration(path) {
    // 獲取原本元件名
    const source = path.node.source.value;
    // 獲取Select as MySelect , Pagination兩個節點
    const specifiers = path.node.specifiers;
    // import specifiers有3種形式,ImportSpecifier ,ImportNamespaceSpecifier,ImportDefaultSpecifier
    // 獲取specifiers型別是否是 名稱空間型別,類似 import * as UI from 'xxx-ui' 這種
    const isImportNamespaceSpecifier = t.isImportNamespaceSpecifier(
      specifiers[0]
    );
    // 獲取specifiers型別是否是 預設匯出型別,類似 import UI2 from 'xxx-ui' 這種
    const isImportDefaultSpecifier = t.isImportDefaultSpecifier(specifiers[0]);
    if (!isImportNamespaceSpecifier && !isImportDefaultSpecifier) {
      const declarations = specifiers.map(specifier => {
        // 快取單個元件名
        let localName = specifier.local.name;
        // 拼接引入路徑
        let newSource = `${source}/${localName}/${localName}.js`;
        // 構造新的ImportDeclaration節點
        return t.importDeclaration(
          [t.importDefaultSpecifier(specifier.local)],
          t.stringLiteral(newSource)
        );
      });
      // 將構造好的新AST替換原來的AST
      path.replaceWithMultiple(declarations);
    }
  }
});
複製程式碼

traverse方法第二個引數傳入的就是我們對具體節點遍歷的處理方法,這裡有個概念需要明確的是,當我們以訪問者身份遍歷節點的時候,我們其實訪問的是路徑path,而非具體某個節點,所以示例中我們我們有2個ImportDeclaration節點,但我們只寫了一個處理方法,因為這裡這個方法會被執行2次。

第四:生成

最後,我們需要將轉換後的AST重新生成程式碼

let newCode = generate(ast).code;
console.log(newCode);
複製程式碼

第五:執行

終端輸入

node ./src/index.js
複製程式碼

可以看到最終我們生成的程式碼

import MySelect from "/MySelect/MySelect.js";
import Pagination from "/Pagination/Pagination.js"; // import UI2 from 'xxx-ui';

import * as UI from 'xxx-ui';
複製程式碼

實際專案引用

我們完成了一個babel外掛,那在專案中如何引入呢?其實,上述所述的步驟程式碼只是從內部剖析了下Babel外掛的處理原理,真正我們在專案中只需要對外暴露一個方法,裡面返回一個包含visitor屬性的物件。

visitor訪問者是一個物件,定義了一系列訪問樹形結構中節點的方法

// myPlugin.js
const babel = require(@babel/core');
const t = require('@babel/types');
export default function() {
  return {
    visitor: {
      ImportDeclaration(path, state) {
        //轉換邏輯
      },
    }
  };
};
複製程式碼

然後在babel-loader的plugin引入

options:{
    plugins:[
        ["myPlugin"]
    ]
}
複製程式碼

原理是啥呢?是因為通過babel-loader引入,Babel裡面core模組提供了transform方法,具體APi可以檢視這裡,只需要傳入visitor物件,該方法據此預設會去做解析轉換生成工作,內部處理邏輯如下:

const visitor = require('visitor.js');
const babel = require('@babel/core');
const result = babel.transform(code, {
	plugins: [visitor],
});
複製程式碼

進階例子

這裡提供一個簡化版的vuereact的示例,有興趣的可以學習下,地址

最後,如果你對多端開發有興趣,我們微店有個小組,從事多端統一開發研究,有興趣的童鞋也可以加入進來看看,裡面有很多Babel相關例項和文章,訪問地址

相關文章