從 Babel 到組件按需引入原理

前言

談到 babel 確定你們都不會感受陌生。html

  • 桌面端組件庫 Element ,藉助 babel-plugin-component ,咱們能夠只引入須要的組件,以達到減少項目體積的目的。
  • 使用 babel-polyfill ,開發者能夠當即使用 ES 規範中的最新特性。
  • 有了插件: transform-vue-jsxreact ,咱們在 vue 和 react 開發中能夠直接使用 JSX 編寫模板。

組件能按需引入究竟是怎麼實現的? Babel 的工做原理是怎樣的呢?vue

帶着疑問,咱們嘗試對其原理深刻探索和理解。node

Babel 編譯的三個階段

Babel 是一個 JavaScript 編譯器。react

和大多數其餘語言的編譯器類似,Babel 的編譯過程可分爲三個階段:typescript

  • 解析 Parse :將代碼字符串解析成抽象語法樹(AST)。簡單來講就是對 JS 代碼進行詞法分析與語法分析。
  • 轉換 Transform :對抽象語法樹進行轉換操做。這裏操做主要是添加、更新及移除。
  • 生成 Generate : 根據變換後的抽象語法樹再生成代碼字符串。

解析 Parse

Babel 會把源代碼抽象出來,變成 ASTnpm

能夠看看 var answer = 6 * 7; 抽象以後的結果。element-ui

{
    "type": "Program", // 根結點
    "body": [
        {
            "type": "VariableDeclaration", // 變量聲明
            "declarations": [
                {
                    "type": "VariableDeclarator", // 變量聲明器
                    "id": {
                        "type": "Identifier",
                        "name": "answer"
                    },
                    "init": {
                        "type": "BinaryExpression", // 表達式
                        "operator": "*", // 操做符是 *
                        "left": {
                            "type": "Literal", // 字面量
                            "value": 6,
                            "raw": "6"
                        },
                        "right": {
                            "type": "Literal",
                            "value": 7,
                            "raw": "7"
                        }
                    }
                }
            ],
            "kind": "var"
        }
    ],
    "sourceType": "script"
}
複製代碼

ProgramVariableDeclarationVariableDeclaratorIdentifierBinaryExpressionLiteral 均爲節點類型。每一個節點都是一個有意義的語法單元。這些節點經過攜帶的屬性描述本身的做用。數組

其中的全部節點名詞,均來源於 ECMA 規範bash

ATS 生成過程分爲兩個步驟:babel

  • 分詞:將代碼字符串分割成語法單元數組 token
  • 語法分析:分析語法單元之間的關聯關係。
分詞

JS 中的語法單元主要包括如下這麼幾種:

  • 關鍵字: constletvar 等。
  • 標識符:if/elsereturnfunction 等。
  • 運算符:+-*/ 等。
  • 數字
  • 空格
  • 註釋

好比下面的代碼生成的語法單元數組:

var answer = 6 * 7;

// Tokens
[
    {
        "type": "Keyword",
        "value": "var"
    },
    {
        "type": "Identifier",
        "value": "answer"
    },
    {
        "type": "Punctuator",
        "value": "="
    },
    {
        "type": "Numeric",
        "value": "6"
    },
    {
        "type": "Punctuator",
        "value": "*"
    },
    {
        "type": "Numeric",
        "value": "7"
    },
    {
        "type": "Punctuator",
        "value": ";"
    }
]
複製代碼

分詞的大體思路:遍歷字符串,經過各類方式(如:正則)匹配當前字符串片斷對應的語法單元類型,而後生成數組 token

語法分析

先了解語法分析的兩個概念:

  • 語句:指一個具有邊界的代碼區域,相鄰的兩個語句之間從語法上來說互不影響,即便調換順序也不會產生語法錯誤。
  • 表達式:指最終有個結果的一小段代碼,它能夠嵌入到另外一個表達式,且包含在語句中。

語法分析就是識別語句和表達式,這是一個遞歸的過程(理解爲深度優先遍歷)。Babel 會在解析過程當中設置一個暫存器,用來暫存當前讀取到的語法單元,若是解析失敗,就會返回以前的暫存點,再按照另外一種方式進行解析,若是解析成功,則將暫存點銷燬,不斷重複以上操做,直到最後生成對應的語法樹。

轉換 Transform

Plugins

插件應用於 Babel 的轉譯過程。若是不使用任何插件,那麼 Babel 會原樣輸出代碼。

Presets

Babel 官方已經針對經常使用環境編寫了一些 preset

Preset 的路徑:

若是 presetnpm 上,你能夠輸入 preset 的名稱,Babel 將檢查是否已經將其安裝到 node_modules 目錄下了

{
  "presets": ["babel-preset-myPreset"]
}
複製代碼

你還能夠指定指向 preset 的絕對或相對路徑。

{
  "presets": ["./myProject/myPreset"]
}
複製代碼

Preset 的排列順序:

Preset 是逆序排列的(從後往前)。

{
  "presets": [
    "a",
    "b",
    "c"
  ]
}
複製代碼

將按以下順序執行: cb 而後是 a

這主要是爲了確保向後兼容,因爲大多數用戶將 es2015 放在 stage-0 以前。

生成 Generate

babel-generator 經過 AST 樹生成 ES5 代碼。

實現一個簡單的按需打包功能

例如 ElementUI 中把 import { Button } from 'element-ui' 轉成 import Button from 'element-ui/lib/button'

能夠先對比下 AST

// import { Button } from 'element-ui'
{
    "type": "Program",
    "body": [
        {
            "type": "ImportDeclaration",
            "specifiers": [
                {
                    "type": "ImportSpecifier",
                    "local": {
                        "type": "Identifier",
                        "name": "Button"
                    },
                    "imported": {
                        "type": "Identifier",
                        "name": "Button"
                    }
                }
            ],
            "source": {
                "type": "Literal",
                "value": "element-ui",
                "raw": "'element-ui'"
            }
        }
    ],
    "sourceType": "module"
}

// import Button from 'element-ui/lib/button'
{
    "type": "Program",
    "body": [
        {
            "type": "ImportDeclaration",
            "specifiers": [
                {
                    "type": "ImportDefaultSpecifier",
                    "local": {
                        "type": "Identifier",
                        "name": "Button"
                    }
                }
            ],
            "source": {
                "type": "Literal",
                "value": "element-ui/lib/button",
                "raw": "'element-ui/lib/button'"
            }
        }
    ],
    "sourceType": "module"
}
複製代碼

能夠發現, specifierstypesourcevalue、raw 不一樣。

而後 ElementUI 官方文檔中,babel-plugin-component 的配置以下:

// 若是 plugins 名稱的前綴爲 'babel-plugin-',你能夠省略 'babel-plugin-' 部分
{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}
複製代碼

直接幹:

import * as babel from '@babel/core'

const str = `import { Button } from 'element-ui'`
const { result } = babel.transform(str, {
    plugins: [
        function({types: t}) {
            return {
                visitor: {
                    ImportDeclaration(path, { opts }) {
                        const { node: { specifiers, source } } = path
                        // 比較 source 的 value 值 與配置文件中的庫名稱
                        if (source.value === opts.libraryName) {
                            const arr = specifiers.map(specifier => (
                                t.importDeclaration(
                                
                                    [t.ImportDefaultSpecifier(specifier.local)],
                                    // 拼接詳細路徑
                                    t.stringLiteral(`${source.value}/lib/${specifier.local.name}`)
                                )
                            ))
                            path.replaceWithMultiple(arr)
                        }
                    }
                }
            }
        }
    ]
})

console.log(result) // import Button from "element-ui/lib/Button";
複製代碼

完美!咱們的第一個 Babel 插件完成了。

你們有沒有對 Babel 有本身的理解了呢?

感謝

若是本文對你有幫助,就點個贊支持下吧!感謝閱讀。

相關文章
相關標籤/搜索