掃二維碼與項(xiàng)目經(jīng)理溝通
我們在微信上24小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
[[383627]]

成都創(chuàng)新互聯(lián)公司是一家企業(yè)級云計(jì)算解決方案提供商,超15年IDC數(shù)據(jù)中心運(yùn)營經(jīng)驗(yàn)。主營GPU顯卡服務(wù)器,站群服務(wù)器,成都服務(wù)器托管,海外高防服務(wù)器,成都機(jī)柜租用,動(dòng)態(tài)撥號VPS,海外云手機(jī),海外云服務(wù)器,海外服務(wù)器租用托管等。
當(dāng)聊到Babel的作用,很多人第一反應(yīng)是:用來實(shí)現(xiàn)API polyfill。
事實(shí)上,Babel作為前端工程化的基石,作用遠(yuǎn)不止這些。
作為一個(gè)龐大的家族,Babel生態(tài)中有很多概念,比如:preset、plugin、runtime等。
這些概念使初學(xué)者對Babel望而生畏,對其理解也止步于webpack的babel-loader配置。
本文會(huì)從Babel的核心功能出發(fā),一步步揭開Babel大家族的神秘面紗,向前端架構(gòu)師邁出一小步。
Babel是什么
作為JS編譯器,Babel接收輸入的JS代碼,經(jīng)過內(nèi)部處理流程,最終輸出修改后的JS代碼。
在Babel內(nèi)部,會(huì)執(zhí)行如下步驟:
從Babel倉庫[1]的源代碼,可以發(fā)現(xiàn):Babel是一個(gè)由幾十個(gè)項(xiàng)目組成的Monorepo。
其中babel-core提供了以上提到的三個(gè)步驟的能力。
在babel-core內(nèi)部,更細(xì)致的講:
要了解第二步,我們需要簡單了解下AST。
AST的結(jié)構(gòu)
進(jìn)入AST explorer[2],選擇@babel/parser作為解析器,在左側(cè)輸入:
- const name = ['ka', 'song'];
可以解析出如下結(jié)構(gòu)的AST,他是JSON格式的樹狀結(jié)構(gòu):
在babel-core內(nèi)部:
所以,整個(gè)Babel底層編譯能力由如下部分構(gòu)成:
當(dāng)我們了解Babel的底層能力后,接下來看看基于這些能力,上層能實(shí)現(xiàn)什么功能?
Babel的上層能力
基于Babel對JS代碼的編譯處理能力,Babel最常見的上層能力為:
由于篇幅有限,這里僅介紹polyfill與「語法轉(zhuǎn)換」相關(guān)功能。
polyfill
作為前端,最常見的Babel生態(tài)的庫想必是@babel/polyfill與@babel/preset-env。
使用@babel/polyfill或@babel/preset-env可以實(shí)現(xiàn)高級語法的降級實(shí)現(xiàn)以及API的polyfill。
從上文我們知道,Babel本身只是JS的編譯器,以上兩者的轉(zhuǎn)換功能是誰實(shí)現(xiàn)的呢?
答案是:core-js
core-js簡介
core-js是一套模塊化的JS標(biāo)準(zhǔn)庫,包括:
[[383630]]
core-js作者Denis Pushkarev
從core-js倉庫[3]看到,core-js也是由多個(gè)庫組成的Monorepo,包括:
我們介紹其中幾個(gè)庫:
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ì)污染全局命名空間和對象原型。
比如上例中修改了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ì)污染全局命名空間與對象原型。
core-js-compat
core-js-compat根據(jù)Browserslist維護(hù)了不同宿主環(huán)境、不同版本下對應(yīng)需要支持特性的集合。
Browserslist[4]提供了不同瀏覽器、node版本下ES特性的支持情況
[[383631]]
Browserslist
比如:
- "browserslist": [
- "not IE 11",
- "maintained node versions"
- ]
代表:非IE11的版本以及所有Node.js基金會(huì)維護(hù)的版本。
@babel/polyfill與core-js關(guān)系
@babel/polyfill可以看作是:core-js加regenerator-runtime。
單獨(dú)使用@babel/polyfill會(huì)將core-js全量導(dǎo)入,造成項(xiàng)目打包體積過大。
為了解決全量引入core-js造成打包體積過大的問題,我們需要配合使用@babel/preset-env。
preset的含義
在介紹@babel/preset-env前,我們先來了解preset的意義。
初始情況下,Babel沒有任何額外能力,其工作流程可以描述為:
- const babel = code => code;
其通過plugin對外提供介入babel-core的能力,類似webpack的plugin對外提供介入webpack編譯流程的能力。
plugin分為幾類:
多個(gè)plugin組合在一起形成的集合,被稱為preset。
@babel/preset-env
使用@babel/preset-env,可以「按需」將core-js中的特性打包,這樣可以顯著減少最終打包的體積。
這里的「按需」,分為兩個(gè)粒度:
我們來依次看下。
宿主環(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的能力。
按使用情況的粒度
更理想的情況是只打包我們使用過的特性。
這時(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)化打包體積
打開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è)文件打包對應(yīng)的module中都將包含_classCallCheck。
為了減少打包體積,更好的方式是:需要使用「輔助方法」的module都從同一個(gè)地方引用,而不是自己維護(hù)一份。
@babel/runtime包含了Babel所有「輔助方法」以及regenerator-runtime。
單純引入@babel/runtime還不行,因?yàn)锽abel不知道何時(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ù)開發(fā)會(huì)接觸的Babel大家族成員。他們包括:
底層
@babel/core(由@babel/parser、@babel/traverse、@babel/types、@babel/generator等組成)
他們提供了Babel編譯JS的能力。
注:這里@babel/core為庫名,前文中babel-core為其在倉庫中對應(yīng)文件名
中層
@babel/plugin-*
Babel對外暴露的API,使開發(fā)者可以介入其編譯JS的能力
上層
@babel/preset-*
日常開發(fā)會(huì)使用的插件集合。
對于立志成為前端架構(gòu)師的同學(xué),Babel是前端工程化的基石,學(xué)懂、會(huì)用他是很有必要的。
能看到這里真不容易,給自己鼓鼓掌吧。
參考資料
[1]Babel倉庫:
https://github.com/babel/babel/tree/main/packages
[2]AST explorer:
https://astexplorer.net
/[3]core-js倉庫:
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]babel playground:
https://babeljs.io/repl#?browsers=&build=&builtIns=false&spec=false&loose=false&code_lz=MYGwhgzhAECCAO9oG8C-Q&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=script&lineWrap=true&presets=env&prettier=false&targets=&version=7.13.7&externalPlugins=babel-plugin-transform-regenerator%406.26.0

我們在微信上24小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流