從0到1完成一個Babel插件

前言

社區裏面有不少關於Babel的文章,有些寫的很好,我本身也受這些文章啓發很大。但我發現一個問題就是,這類文章一進來就講了不少babel底層的概念,說實話對基礎不深的一些童鞋來講,看完以後理解起來仍是有必定難度的,最重要的是看完了以後,本身並不知道如何去寫一個Babel插件,於是這促使了如何從0到1完成一個babel插件這篇文章的編寫,學習完本篇文章,指望是你們能對Babel有一個總體的認識,知道Babel是什麼?Babel是如何運做的?而且本身能實現一個簡單的Babel插件。vue

什麼是Babel

Babel是一個JavaScript編譯器,意思就是說你爲Babel提供一些代碼,Babel作一些轉換,給你返回一些新的代碼。好比,咱們常見的將ES5+的代碼轉換成ES5+以前的一些代碼。node

Babel的處理步驟

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

解析

解析又通過詞法分析語法分析兩個步驟,將輸入的代碼生成抽象語法數(AST),AST能夠理解爲就是描述一段代碼的節點樹,看以下這個例子:git

咱們輸入github

const a = 1
複製代碼

通過解析(parse),生成以下結構的節點樹(爲了方便觀看,去掉了一些代表節點位置信息的屬性),詳細的能夠經過這個工具查看json

{
	"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插件真正處理的地方bash

生成

通過轉換以後的AST仍是AST,因此咱們還須要將AST生成字符串形式的代碼babel

實戰

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相關實例和文章,訪問地址

相關文章
相關標籤/搜索