從 Babel 到組件按需引入原理

友情鏈接

前言

談到 babel 肯定大家都不會(huì)感覺陌生。

  • 桌面端組件庫(kù) Element 狡蝶,借助 babel-plugin-component 佳吞,我們可以只引入需要的組件,以達(dá)到減小項(xiàng)目體積的目的魔眨。
  • 使用 babel-polyfill 媳维,開發(fā)者可以立即使用 ES 規(guī)范中的最新特性。
  • 有了插件: transform-vue-jsx 遏暴、 react 侄刽,我們?cè)?vue 和 react 開發(fā)中可以直接使用 JSX 編寫模板。

組件能按需引入到底是怎么實(shí)現(xiàn)的朋凉? Babel 的工作原理是怎樣的呢唠梨?

帶著疑問(wèn),我們嘗試對(duì)其原理深入探索和理解侥啤。

Babel 編譯的三個(gè)階段

Babel 是一個(gè) JavaScript 編譯器当叭。

和大多數(shù)其他語(yǔ)言的編譯器相似,Babel 的編譯過(guò)程可分為三個(gè)階段:

  • 解析 Parse :將代碼字符串解析成抽象語(yǔ)法樹(AST)盖灸。簡(jiǎn)單來(lái)說(shuō)就是對(duì) JS 代碼進(jìn)行詞法分析與語(yǔ)法分析蚁鳖。
  • 轉(zhuǎn)換 Transform :對(duì)抽象語(yǔ)法樹進(jìn)行轉(zhuǎn)換操作。這里操作主要是添加赁炎、更新及移除醉箕。
  • 生成 Generate : 根據(jù)變換后的抽象語(yǔ)法樹再生成代碼字符串钾腺。

解析 Parse

Babel 會(huì)把源代碼抽象出來(lái),變成 AST 讥裤。

可以看看 var answer = 6 * 7; 抽象之后的結(jié)果放棒。

{
    "type": "Program", // 根結(jié)點(diǎn)
    "body": [
        {
            "type": "VariableDeclaration", // 變量聲明
            "declarations": [
                {
                    "type": "VariableDeclarator", // 變量聲明器
                    "id": {
                        "type": "Identifier",
                        "name": "answer"
                    },
                    "init": {
                        "type": "BinaryExpression", // 表達(dá)式
                        "operator": "*", // 操作符是 *
                        "left": {
                            "type": "Literal", // 字面量
                            "value": 6,
                            "raw": "6"
                        },
                        "right": {
                            "type": "Literal",
                            "value": 7,
                            "raw": "7"
                        }
                    }
                }
            ],
            "kind": "var"
        }
    ],
    "sourceType": "script"
}

ProgramVariableDeclaration 己英、 VariableDeclarator 间螟、 IdentifierBinaryExpression 损肛、 Literal 均為節(jié)點(diǎn)類型厢破。每個(gè)節(jié)點(diǎn)都是一個(gè)有意義的語(yǔ)法單元。這些節(jié)點(diǎn)通過(guò)攜帶的屬性描述自己的作用治拿。

其中的所有節(jié)點(diǎn)名詞摩泪,均來(lái)源于 ECMA 規(guī)范

ATS 生成過(guò)程分為兩個(gè)步驟:

  • 分詞:將代碼字符串分割成語(yǔ)法單元數(shù)組 token 劫谅。
  • 語(yǔ)法分析:分析語(yǔ)法單元之間的關(guān)聯(lián)關(guān)系见坑。
分詞

JS 中的語(yǔ)法單元主要包括以下這么幾種:

  • 關(guān)鍵字: constlet 捏检、 var 等荞驴。
  • 標(biāo)識(shí)符:if/elsereturn 未檩、 function 等戴尸。
  • 運(yùn)算符:+ 粟焊、 - 冤狡、 */ 等项棠。
  • 數(shù)字
  • 空格
  • 注釋

比如下面的代碼生成的語(yǔ)法單元數(shù)組:

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": ";"
    }
]

分詞的大致思路:遍歷字符串悲雳,通過(guò)各種方式(如:正則)匹配當(dāng)前字符串片段對(duì)應(yīng)的語(yǔ)法單元類型,然后生成數(shù)組 token 香追。

語(yǔ)法分析

先了解語(yǔ)法分析的兩個(gè)概念:

  • 語(yǔ)句:指一個(gè)具備邊界的代碼區(qū)域合瓢,相鄰的兩個(gè)語(yǔ)句之間從語(yǔ)法上來(lái)講互不影響,即使調(diào)換順序也不會(huì)產(chǎn)生語(yǔ)法錯(cuò)誤透典。
  • 表達(dá)式:指最終有個(gè)結(jié)果的一小段代碼晴楔,它可以嵌入到另一個(gè)表達(dá)式,且包含在語(yǔ)句中峭咒。

語(yǔ)法分析就是識(shí)別語(yǔ)句和表達(dá)式税弃,這是一個(gè)遞歸的過(guò)程(理解為深度優(yōu)先遍歷)。Babel 會(huì)在解析過(guò)程中設(shè)置一個(gè)暫存器凑队,用來(lái)暫存當(dāng)前讀取到的語(yǔ)法單元则果,如果解析失敗,就會(huì)返回之前的暫存點(diǎn),再按照另一種方式進(jìn)行解析西壮,如果解析成功遗增,則將暫存點(diǎn)銷毀,不斷重復(fù)以上操作款青,直到最后生成對(duì)應(yīng)的語(yǔ)法樹做修。

轉(zhuǎn)換 Transform

Plugins

插件應(yīng)用于 Babel 的轉(zhuǎn)譯過(guò)程。如果不使用任何插件可都,那么 Babel 會(huì)原樣輸出代碼缓待。

Presets

Babel 官方已經(jīng)針對(duì)常用環(huán)境編寫了一些 preset

Preset 的路徑:

如果 presetnpm 上,你可以輸入 preset 的名稱渠牲,Babel 將檢查是否已經(jīng)將其安裝到 node_modules 目錄下了

{
  "presets": ["babel-preset-myPreset"]
}

你還可以指定指向 preset 的絕對(duì)或相對(duì)路徑旋炒。

{
  "presets": ["./myProject/myPreset"]
}

Preset 的排列順序:

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

{
  "presets": [
    "a",
    "b",
    "c"
  ]
}

將按如下順序執(zhí)行: c 签杈、b 然后是 a瘫镇。

這主要是為了確保向后兼容,由于大多數(shù)用戶將 es2015 放在 stage-0 之前答姥。

生成 Generate

babel-generator 通過(guò) AST 樹生成 ES5 代碼铣除。

實(shí)現(xiàn)一個(gè)簡(jiǎn)單的按需打包功能

例如 ElementUI 中把 import { Button } from 'element-ui' 轉(zhuǎn)成 import Button from 'element-ui/lib/button'

可以先對(duì)比下 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"
}

可以發(fā)現(xiàn), 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 值 與配置文件中的庫(kù)名稱
                        if (source.value === opts.libraryName) {
                            const arr = specifiers.map(specifier => (
                                t.importDeclaration(
                                
                                    [t.ImportDefaultSpecifier(specifier.local)],
                                    // 拼接詳細(xì)路徑
                                    t.stringLiteral(`${source.value}/lib/${specifier.local.name}`)
                                )
                            ))
                            path.replaceWithMultiple(arr)
                        }
                    }
                }
            }
        }
    ]
})

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

完美郎嫁!我們的第一個(gè) Babel 插件完成了。

大家有沒(méi)有對(duì) Babel 有自己的理解了呢祈噪?

感謝

如果本文對(duì)你有幫助泽铛,就點(diǎn)個(gè)贊支持下吧!感謝閱讀辑鲤。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末盔腔,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子月褥,更是在濱河造成了極大的恐慌弛随,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宁赤,死亡現(xiàn)場(chǎng)離奇詭異舀透,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)礁击,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門盐杂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)逗载,“玉大人,你說(shuō)我怎么就攤上這事链烈±髡澹” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵强衡,是天一觀的道長(zhǎng)擦秽。 經(jīng)常有香客問(wèn)我,道長(zhǎng)漩勤,這世上最難降的妖魔是什么感挥? 我笑而不...
    開封第一講書人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮越败,結(jié)果婚禮上触幼,老公的妹妹穿的比我還像新娘。我一直安慰自己究飞,他們只是感情好置谦,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著亿傅,像睡著了一般媒峡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上葵擎,一...
    開封第一講書人閱讀 52,246評(píng)論 1 308
  • 那天谅阿,我揣著相機(jī)與錄音,去河邊找鬼酬滤。 笑死签餐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的敏晤。 我是一名探鬼主播贱田,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼缅茉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼嘴脾!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起蔬墩,我...
    開封第一講書人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤译打,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后拇颅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體奏司,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年樟插,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了韵洋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片竿刁。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖搪缨,靈堂內(nèi)的尸體忽然破棺而出食拜,到底是詐尸還是另有隱情,我是刑警寧澤副编,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布负甸,位于F島的核電站,受9級(jí)特大地震影響痹届,放射性物質(zhì)發(fā)生泄漏呻待。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一队腐、第九天 我趴在偏房一處隱蔽的房頂上張望蚕捉。 院中可真熱鬧,春花似錦柴淘、人聲如沸鱼冀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)千绪。三九已至,卻和暖如春梗脾,著一層夾襖步出監(jiān)牢的瞬間荸型,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工炸茧, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瑞妇,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓梭冠,卻偏偏與公主長(zhǎng)得像辕狰,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子控漠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359