手把手教你開發一個babel-plugin

需求

在最近的開發過程當中,不一樣的項目、不一樣的頁面都須要用到某種UI控件,因而很天然的將這些UI控件拆出來,單獨創建了一個代碼庫進行維護。下面是個人組件庫大體的目錄結構以下:javascript

...
- lib
    - components
        - componentA
            - index.vue
        - componentB
            - index.vue
        - componentC
            - index.vue
- index.js
...

整個組件庫的出口在index.js,裏面的內容差很少是下面這樣的:vue

import A from './lib/componentA';
import B from './lib/componentB';
import C from './lib/componentC';

export {
    A,
    B,
    C
}

個人代碼庫的name爲:kb-bi-vue-component。在項目中引用這個組件庫的時候,代碼以下:java

import { A, B } from 'kb-bi-vue-component';
....

這個時候,問題出現了,我在頁面中,僅僅使用了AB兩個組件,可是頁面打包後,整個組件庫的代碼都會被打進來,增長了產出的體積,包括了很多的冗餘代碼。很容易想到的一個解決方案是按照如下的方式引用組件。node

import A from 'kb-bi-vue-component/lib/componentA';
import B from 'kb-bi-vue-component/lib/componentB';

這種方法雖然解決了問題,我想引用哪一個組件,就引用哪一個組件,不會有多餘的代碼。可是我總以爲這種寫法看起來不太舒服。有沒有還能像第一種寫法同樣引用組件庫,而且只引用須要的組件呢?寫一個babel-plugin好了,自動將第一種寫法轉換成第二種寫法。git

Babel的原理

本文只是簡單介紹。想要深刻理解代碼編譯,請學習<<編譯原理>>

這裏有一個不錯的Babel教程:https://github.com/jamiebuild...github

Babel是Javascript編譯器,更確切地說是源碼到源碼的編譯器,一般也叫作『轉換編譯器』。也就是說,你給Babel提供一些Javascript代碼,Babel更改這下代碼,而後返回給你新生成的代碼。數組

AST

在這整個過程當中,都是圍繞着抽象語法樹(AST)來進行的。在Javascritp中,AST,簡單來講,就是一個記錄着代碼語法結構的Object。好比下面的代碼:babel

function square(n) {
  return n * n;
}

轉換成AST後以下,函數

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

AST是分層的,由一個一個的 節點(Node) 組成。如:工具

{
  ...
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  ...
}
{
  type: "Identifier",
  name: ...
}

每個節點都有一個必需的 type 字段表示節點的類型。如上面的FunctionDeclaration

Identifier等等。每種類型的節點都會有本身的屬性。

Babel的工做過程

Babel的處理過程主要爲3個:解析(parse)轉換(transform)生成(generate)

  • 解析

    解析主要包含兩個過程:詞法分析和語法分析,輸入是代碼字符串,輸出是AST。

  • 轉換

    處理AST。處理工具、插件等就是在這個過程當中介入,將代碼按照需求進行轉換。

  • 生成

    遍歷AST,輸出代碼字符串。

解析和生成過程,都有Babel都爲咱們處理得很好了,咱們要作的就是在 轉換 過程當中搞事情,進行個性化的定製開發。

開發一個babel-plugin

這裏有詳細的介紹: https://github.com/jamiebuild...

開發方式概述

首先,須要大體瞭解一下babel-plugin的開發方法。

babel使用一種 訪問者模式 來遍歷整棵語法樹,即遍歷進入到每個Node節點時,能夠說咱們在「訪問」這個節點。訪問者就是一個對象,定義了在一個樹狀結構中獲取具體節點的方法。簡單來講,咱們能夠在訪問者中,使用Node的type來定義一個hook函數,每一次遍歷到對應type的Node時,hook函數就會被觸發,咱們能夠在這個hook函數中,修改、查看、替換、刪除這個節點。提及來很抽象,直接看下面的內容吧。

開始開發吧

  • 下面,根據咱們的需求,來開發一個plugin。怎麼配置使用本身的babel-plugin呢?個人項目中,是使用.babelrc來配置babel的,以下:
{
    "presets": [
        ["es2015"],
        ["stage-0"]
    ]
}

上面的配置中,只有兩個預設,並無使用插件。首先加上插件的配置。因爲是在本地開發,插件直接寫的本地的相對地址。

{
    "presets": [
        ["es2015"],
        ["stage-0"]
    ],
    "plugins":["./my-import-babel-plugin"]
}

僅僅像上面這樣是有問題的,由於需求是須要針對具體的library,因此確定是須要傳入參數的。改爲下面這樣:

{
    "presets": [
        ["es2015"],
        ["stage-0"]
    ],
    "plugins":[
        ["./my-import-babel-plugin", { "libraryName": "kb-bi-vue-component", "alias": "kb-bi-vue-component/lib/components"}]
    ]
}

咱們給plugin傳了一個參數,libraryName表示須要處理的library,alias表示組件在組件庫內部的路徑。

  • 下面是插件的代碼./my-import-babel-plugin.js
module.exports = function ({ types: t }) {
    return {
        visitor: {
            ImportDeclaration(path, source){
                const { opts: { libraryName, alias } } = source;
                if (!t.isStringLiteral(path.node.source, { value: libraryName })) {
                    return;
                }
                console.log(path.node);
                // todo
            }
        }    
    }
}

函數的參數爲babel對象,對象中的types是一個用於 AST 節點的 Lodash 式工具庫,它包含了構造、驗證以及變換 AST 節點的方法。 該工具庫包含考慮周到的工具方法,對編寫處理AST邏輯很是有用。咱們單獨把這個types拿出來。返回的visitor就是咱們上文提到的訪問者對象。此次的需求是對 import 語句的修改,因此咱們在visitor中定義了import的type:ImportDeclaration。這樣,當babel處理到代碼裏的import語句時,就會走到這個ImportDeclaration函數裏面來。

這裏查看Babel定義的全部的AST Node: https://github.com/babel/babe...

ImportDeclaration接受兩個參數,

  1. path表示當前訪問的路徑,path.node就能取到當前訪問的Node.
  2. source表示PluginPass,即傳遞給當前plugin的其餘信息,包括當前編譯的文件、代碼字符串以及咱們在.babelrc中傳入的參數等。

在插件的代碼中,咱們首先取到了傳入插件的參數。接着,判斷若是不是咱們須要處理的library,就直接返回了

這裏能夠查看babel.types的使用方法: https://babeljs.io/docs/en/ba...
  • 假設咱們的業務代碼中的代碼以下:
...
import { A, B } from 'kb-bi-vue-component'
...

咱們運行一下打包工具,輸出一下path.node,能夠看到,當前訪問的Node以下:

Node {
    type: 'ImportDeclaration',
    start: 9,
    end: 51,
    loc: SourceLocation {
        start: Position {
            line: 10,
            column: 0
        },
        end: Position {
            line: 10,
            column: 42
        }
    },
    specifiers: [Node {
            type: 'ImportSpecifier',
            start: 18,
            end: 19,
            loc: [Object],
            imported: [Object],
            local: [Object]
        },
        Node {
            type: 'ImportSpecifier',
            start: 21,
            end: 22,
            loc: [Object],
            imported: [Object],
            local: [Object]
        }
    ],
    source: Node {
        type: 'StringLiteral',
        start: 30,
        end: 51,
        loc: SourceLocation {
            start: [Object],
            end: [Object]
        },
        extra: {
            rawValue: 'kb-bi-vue-component',
            raw: '\'kb-bi-vue-component\''
        },
        value: 'kb-bi-vue-component'
    }
}

稍微解釋一下這個Node. specifiers是一個數組,包含兩個Node,對應的是代碼import後面的兩個參數AB。這兩個Node的local值都是Identifier類型的Node。source表示的是代碼from後面的library。

  • 接下來,按照需求把這個ImportDeclaration類型的Node替換掉,換成咱們想要的。使用path.replaceWithMultiple這個方法來替換一個Node。此方法接受一個Node數組。因此咱們首先須要構造出Node,裝進一個數組裏,而後扔給這個path.replaceWithMultiple方法。

    查閱文檔,

    t.importDeclaration(specifiers, source)
    
    specifiers: Array<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier> (required)
    source: StringLiteral (required)

    能夠經過t.importDeclaration來構造importNode,參數如上所示。構造importNode,須要先構造其參數須要的Node。最終,修改插件的代碼以下:

    module.exports = function ({ types: t }) {
        return {
            visitor: {
                ImportDeclaration(path, source) {
                    const { opts: { libraryName, alias } } = source;
                    if (!t.isStringLiteral(path.node.source, { value: libraryName })) {
                        return;
                    }
                    const newImports = path.node.specifiers.map( item => {
                        return t.importDeclaration([t.importDefaultSpecifier(item.local)], t.stringLiteral(`${alias}/${item.local.name}`))
                    });
                    path.replaceWithMultiple(newImports);
                }
            }
        }
    }

    開發基本結束

    好了,一個babel-plugin開發完成了。咱們成功的實現瞭如下的編譯:

    import { A, B } from 'kb-bi-vue-component';
    
    ↓ ↓ ↓ ↓ ↓ ↓
    
    import A from 'kb-bi-vue-component/lib/components/A';
    import B from 'kb-bi-vue-component/lib/components/B';

    babel在工做時,會優先執行.babelrc中的plugins,接着纔會執行presets。咱們優先將源代碼進行了轉換,再使用babel去轉換爲es5的代碼,整個過程是沒有問題的。

    固然,這是最簡單的babel-plugin,還有不少其餘狀況沒有處理,好比下面這種,轉換後就不符合預期。

    import { A as aaa, B } from 'kb-bi-vue-component';
    
    ↓ ↓ ↓ ↓ ↓ ↓
    
    import aaa from 'kb-bi-vue-component/lib/components/aaa';
    import B from 'kb-bi-vue-component/lib/components/B';

    要完成一個高質量的babel-plugin,還有不少的工做要作。

    附:阿里已經開源了一個成熟的babel-plugin-import

    參考連接:

    一、https://github.com/jamiebuild...
    二、https://babeljs.io/docs/en/ba...

相關文章
相關標籤/搜索