友情鏈接
前言
談到 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"
}
Program
、 VariableDeclaration
己英、 VariableDeclarator
间螟、 Identifier
、 BinaryExpression
损肛、 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)鍵字:
const
、let
捏检、var
等荞驴。 - 標(biāo)識(shí)符:
if/else
、return
未檩、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
的路徑:
如果 preset
在 npm
上,你可以輸入 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), specifiers
的 type
和 source
的 value鹦付、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è)贊支持下吧!感謝閱讀辑鲤。