解剖Babel —— 向前端架構(gòu)師邁出一小步
解剖Babel —— 向前端架構(gòu)師邁出一小步
當(dāng)聊到Babel
的作用,很多人第一反應(yīng)是:用來(lái)實(shí)現(xiàn)API polyfill
。
事實(shí)上,Babel
作為前端工程化的基石,作用遠(yuǎn)不止這些。
作為一個(gè)龐大的家族,Babel
生態(tài)中有很多概念,比如:preset
、plugin
、runtime
等。
這些概念使初學(xué)者對(duì)Babel
望而生畏,對(duì)其理解也止步于webpack
的babel-loader
配置。
本文會(huì)從Babel
的核心功能出發(fā),一步步揭開(kāi)Babel
大家族的神秘面紗,向前端架構(gòu)師邁出一小步。
Babel是什么
Babel 是一個(gè) JavaScript 編譯器。
作為JS
編譯器,Babel
接收輸入的JS
代碼,經(jīng)過(guò)內(nèi)部處理流程,最終輸出修改后的JS
代碼。
在Babel
內(nèi)部,會(huì)執(zhí)行如下步驟:
- 將
Input Code
解析為AST
(抽象語(yǔ)法樹(shù)),這一步稱為parsing
- 編輯
AST
,這一步稱為transforming
- 將編輯后的
AST
輸出為Output Code
,這一步稱為printing
從Babel倉(cāng)庫(kù)[1]的源代碼,可以發(fā)現(xiàn):Babel
是一個(gè)由幾十個(gè)項(xiàng)目組成的Monorepo
。
其中babel-core
提供了以上提到的三個(gè)步驟的能力。
在babel-core
內(nèi)部,更細(xì)致的講:
babel-parser
實(shí)現(xiàn)第一步babel-generator
實(shí)現(xiàn)第三步
要了解第二步,我們需要簡(jiǎn)單了解下AST
。
AST的結(jié)構(gòu)
進(jìn)入AST explorer[2],選擇@babel/parser
作為解析器,在左側(cè)輸入:
const name = ['ka', 'song'];
可以解析出如下結(jié)構(gòu)的AST
,他是JSON
格式的樹(shù)狀結(jié)構(gòu):
在babel-core
內(nèi)部:
babel-traverse
可以通過(guò)「深度優(yōu)先」的方式遍歷AST
樹(shù)- 對(duì)于遍歷到的每條路徑,
babel-types
提供用于修改AST
節(jié)點(diǎn)的節(jié)點(diǎn)類型數(shù)據(jù)
所以,整個(gè)Babel
底層編譯能力由如下部分構(gòu)成:
當(dāng)我們了解Babel
的底層能力后,接下來(lái)看看基于這些能力,上層能實(shí)現(xiàn)什么功能?
Babel的上層能力
基于Babel
對(duì)JS
代碼的編譯處理能力,Babel
最常見(jiàn)的上層能力為:
polyfill
DSL
轉(zhuǎn)換(比如解析JSX
)- 語(yǔ)法轉(zhuǎn)換(比如將高級(jí)語(yǔ)法解析為當(dāng)前可用的實(shí)現(xiàn))
由于篇幅有限,這里僅介紹polyfill
與「語(yǔ)法轉(zhuǎn)換」相關(guān)功能。
polyfill
作為前端,最常見(jiàn)的Babel
生態(tài)的庫(kù)想必是@babel/polyfill
與@babel/preset-env
。
使用@babel/polyfill
或@babel/preset-env
可以實(shí)現(xiàn)高級(jí)語(yǔ)法的降級(jí)實(shí)現(xiàn)以及API
的polyfill
。
從上文我們知道,Babel
本身只是JS
的編譯器,以上兩者的轉(zhuǎn)換功能是誰(shuí)實(shí)現(xiàn)的呢?
答案是:core-js
core-js簡(jiǎn)介
core-js
是一套模塊化的JS
標(biāo)準(zhǔn)庫(kù),包括:
- 一直到
ES2021
的polyfill
promise
、symbols
、iterators
等一些特性的實(shí)現(xiàn)ES
提案中的特性實(shí)現(xiàn)- 跨平臺(tái)的
WHATWG / W3C
特性,比如URL
從core-js倉(cāng)庫(kù)[3]看到,core-js
也是由多個(gè)庫(kù)組成的Monorepo
,包括:
- core-js-builder
- core-js-bundle
- core-js-compat
- core-js-pure
- core-js
我們介紹其中幾個(gè)庫(kù):
core-js
core-js
提供了polyfill
的核心實(shí)現(xiàn)。
import 'core-js/features/array/from';
import 'core-js/features/array/flat';
import 'core-js/features/set';
import 'core-js/features/promise';
Array.from(new Set([1, 2, 3, 2, 1])); // => [1, 2, 3]
[1, [2, 3], [4, [5]]].flat(2); // => [1, 2, 3, 4, 5]
Promise.resolve(32).then(x => console.log(x)); // => 32
直接使用core-js
會(huì)污染全局命名空間和對(duì)象原型。
比如上例中修改了Array
的原型以支持?jǐn)?shù)組實(shí)例的flat
方法。
core-js-pure
core-js-pure
提供了獨(dú)立的命名空間:
import from from 'core-js-pure/features/array/from';
import flat from 'core-js-pure/features/array/flat';
import Set from 'core-js-pure/features/set';
import Promise from 'core-js-pure/features/promise';
from(new Set([1, 2, 3, 2, 1])); // => [1, 2, 3]
flat([1, [2, 3], [4, [5]]], 2); // => [1, 2, 3, 4, 5]
Promise.resolve(32).then(x => console.log(x)); // => 32
這樣使用不會(huì)污染全局命名空間與對(duì)象原型。
core-js-compat
core-js-compat
根據(jù)Browserslist
維護(hù)了不同宿主環(huán)境、不同版本下對(duì)應(yīng)需要支持特性的集合。
Browserslist[4]提供了不同瀏覽器、node
版本下ES
特性的支持情況
比如:
"browserslist": [
"not IE 11",
"maintained node versions"
]
代表:非IE
11的版本以及所有Node.js
基金會(huì)維護(hù)的版本。
@babel/polyfill與core-js關(guān)系
@babel/polyfill
可以看作是:core-js
加regenerator-runtime
。
regenerator-runtime
是generator
以及async/await
的運(yùn)行時(shí)依賴
單獨(dú)使用@babel/polyfill
會(huì)將core-js
全量導(dǎo)入,造成項(xiàng)目打包體積過(guò)大。
從Babel v7.4.0[5]開(kāi)始,@babel/polyfill
被廢棄了,可以直接引用core-js
與regenerator-runtime
替代
為了解決全量引入core-js
造成打包體積過(guò)大的問(wèn)題,我們需要配合使用@babel/preset-env
。
preset的含義
在介紹@babel/preset-env
前,我們先來(lái)了解preset
的意義。
初始情況下,Babel
沒(méi)有任何額外能力,其工作流程可以描述為:
const babel = code => code;
其通過(guò)plugin
對(duì)外提供介入babel-core
的能力,類似webpack
的plugin
對(duì)外提供介入webpack
編譯流程的能力。
plugin
分為幾類:
@babel/plugin-syntax-*
語(yǔ)法相關(guān)插件,用于新的語(yǔ)法支持。比如babel-plugin-syntax-decorators[6]提供decorators
的語(yǔ)法支持@babel/plugin-proposal-*
用于ES
提案的特性支持,比如babel-plugin-proposal-optional-chaining
是可選鏈操作符
特性支持@babel/plugin-transform-*
用于轉(zhuǎn)換代碼,transform
插件內(nèi)部會(huì)使用對(duì)應(yīng)syntax
插件
多個(gè)plugin
組合在一起形成的集合,被稱為preset
。
@babel/preset-env
使用@babel/preset-env
,可以「按需」將core-js
中的特性打包,這樣可以顯著減少最終打包的體積。
這里的「按需」,分為兩個(gè)粒度:
- 宿主環(huán)境的粒度。根據(jù)不同宿主環(huán)境將該環(huán)境下所需的所有特性打包
- 按使用情況的粒度。僅僅將使用了的特性打包
我們來(lái)依次看下。
宿主環(huán)境的粒度
當(dāng)我們按如下參數(shù)在項(xiàng)目目錄下配置browserslist
文件(或在@babel/preset-env
的targets
屬性內(nèi)設(shè)置,或在package.json
的browserslist
屬性中設(shè)置):
not IE 11
maintained node versions
會(huì)將「非IE11」且「所有Node.js基金會(huì)維護(hù)的node版本」下需要的特性打入最終的包。
顯然這是利用了剛才介紹的core-js
這個(gè)Monorepo
下的core-js-compat
的能力。
按使用情況的粒度
更理想的情況是只打包我們使用過(guò)的特性。
這時(shí)候可以設(shè)置@babel/preset-env
的useBuiltIns
屬性為usage
。
比如:
a.js
:
var a = new Promise();
b.js
:
var b = new Map();
當(dāng)宿主環(huán)境不支持promise
與Map
時(shí),輸出的文件為:
a.js
:
import "core-js/modules/es.promise";
var a = new Promise();
b.js
:
import "core-js/modules/es.map";
var b = new Map();
當(dāng)宿主環(huán)境支持這兩個(gè)特性時(shí),輸出的文件為:
a.js
:
var a = new Promise();
b.js
:
var b = new Map();
進(jìn)一步優(yōu)化打包體積
打開(kāi)babel playground[7],輸入:
class App {}
會(huì)發(fā)現(xiàn)編譯出的結(jié)果為:
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var App = function App() {
"use strict";
_classCallCheck(this, App);
};
其中_classCallCheck
為輔助方法。
如果多個(gè)文件都使用了class
特性,那么每個(gè)文件打包對(duì)應(yīng)的module
中都將包含_classCallCheck
。
為了減少打包體積,更好的方式是:需要使用「輔助方法」的module
都從同一個(gè)地方引用,而不是自己維護(hù)一份。
@babel/runtime
包含了Babel
所有「輔助方法」以及regenerator-runtime
。
單純引入@babel/runtime
還不行,因?yàn)?code>Babel不知道何時(shí)引用@babel/runtime
中的「輔助方法」。
所以,還需要引入@babel/plugin-transform-runtime
。
這個(gè)插件會(huì)在編譯時(shí)將所有使用「輔助方法」的地方從「自己維護(hù)一份」改為從@babel/runtime
中引入。
所以我們需要將@babel/plugin-transform-runtime
置為devDependence
,因?yàn)樗诰幾g時(shí)使用。
將@babel/runtime
置為dependence
,因?yàn)樗谶\(yùn)行時(shí)使用。
總結(jié)
本文從底層向上介紹了前端日常業(yè)務(wù)開(kāi)發(fā)會(huì)接觸的Babel
大家族成員。他們包括:
底層
@babel/core
(由@babel/parser
、@babel/traverse
、@babel/types
、@babel/generator
等組成)
他們提供了Babel
編譯JS
的能力。
注:這里@babel/core
為庫(kù)名,前文中babel-core
為其在倉(cāng)庫(kù)中對(duì)應(yīng)文件名
中層
@babel/plugin-*
Babel
對(duì)外暴露的API
,使開(kāi)發(fā)者可以介入其編譯JS
的能力
上層
@babel/preset-*
日常開(kāi)發(fā)會(huì)使用的插件集合。
對(duì)于立志成為前端架構(gòu)師的同學(xué),Babel
是前端工程化的基石,學(xué)懂、會(huì)用他是很有必要的。
能看到這里真不容易,給自己鼓鼓掌吧。
參考資料
[1]
Babel倉(cāng)庫(kù):?https://github.com/babel/babel/tree/main/packages
[2]
AST explorer:?https://astexplorer.net/
[3]
core-js倉(cāng)庫(kù):?https://github.com/zloirock/core-js/tree/master/packages
[4]
Browserslist:?https://github.com/browserslist/browserslist
[5]
Babel v7.4.0:?https://babeljs.io/docs/en/babel-polyfill#docsNav
[6]
babel-plugin-syntax-decorators:?https://github.com/babel/babel/tree/main/packages/babel-plugin-syntax-decorators
[7]
本文摘自 :https://www.cnblogs.com/