當前位置:首頁 > IT技術 > 其他 > 正文

解剖Babel —— 向前端架構師邁出一小步
2022-05-11 11:07:31

解剖Babel —— 向前端架構師邁出一小步

?

解剖Babel —— 向前端架構師邁出一小步

當聊到Babel的作用,很多人第一反應是:用來實現(xiàn)API polyfill。

事實上,Babel作為前端工程化的基石,作用遠不止這些。

作為一個龐大的家族,Babel生態(tài)中有很多概念,比如:preset、pluginruntime等。

這些概念使初學者對Babel望而生畏,對其理解也止步于webpackbabel-loader配置。

本文會從Babel的核心功能出發(fā),一步步揭開Babel大家族的神秘面紗,向前端架構師邁出一小步。

Babel是什么

Babel 是一個 JavaScript 編譯器。

作為JS編譯器,Babel接收輸入的JS代碼,經(jīng)過內(nèi)部處理流程,最終輸出修改后的JS代碼。

Babel內(nèi)部,會執(zhí)行如下步驟:

  1. Input Code解析為AST(抽象語法樹),這一步稱為parsing
  2. 編輯AST,這一步稱為transforming
  3. 將編輯后的AST輸出為Output Code,這一步稱為printing

從Babel倉庫[1]的源代碼,可以發(fā)現(xiàn):Babel是一個由幾十個項目組成的Monorepo。

其中babel-core提供了以上提到的三個步驟的能力。

babel-core內(nèi)部,更細致的講:

  • babel-parser實現(xiàn)第一步
  • babel-generator實現(xiàn)第三步

要了解第二步,我們需要簡單了解下AST

AST的結構

進入AST explorer[2],選擇@babel/parser作為解析器,在左側輸入:

const name = ['ka', 'song'];

可以解析出如下結構的AST,他是JSON格式的樹狀結構:

babel-core內(nèi)部:

  • babel-traverse可以通過「深度優(yōu)先」的方式遍歷AST
  • 對于遍歷到的每條路徑,babel-types提供用于修改AST節(jié)點的節(jié)點類型數(shù)據(jù)

所以,整個Babel底層編譯能力由如下部分構成:

當我們了解Babel的底層能力后,接下來看看基于這些能力,上層能實現(xiàn)什么功能?

Babel的上層能力

基于BabelJS代碼的編譯處理能力,Babel最常見的上層能力為:

  • polyfill
  • DSL轉換(比如解析JSX
  • 語法轉換(比如將高級語法解析為當前可用的實現(xiàn))

由于篇幅有限,這里僅介紹polyfill與「語法轉換」相關功能。

polyfill

作為前端,最常見的Babel生態(tài)的庫想必是@babel/polyfill@babel/preset-env。

使用@babel/polyfill@babel/preset-env可以實現(xiàn)高級語法的降級實現(xiàn)以及APIpolyfill

從上文我們知道,Babel本身只是JS的編譯器,以上兩者的轉換功能是誰實現(xiàn)的呢?

答案是:core-js

core-js簡介

core-js是一套模塊化的JS標準庫,包括:

  • 一直到ES2021polyfill
  • promise、symbols、iterators等一些特性的實現(xiàn)
  • ES提案中的特性實現(xiàn)
  • 跨平臺的WHATWG / W3C特性,比如URL

從core-js倉庫[3]看到,core-js也是由多個庫組成的Monorepo,包括:

  • core-js-builder
  • core-js-bundle
  • core-js-compat
  • core-js-pure
  • core-js

我們介紹其中幾個庫:

core-js

core-js提供了polyfill的核心實現(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會污染全局命名空間和對象原型。

比如上例中修改了Array的原型以支持數(shù)組實例的flat方法。

core-js-pure

core-js-pure提供了獨立的命名空間:

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

這樣使用不會污染全局命名空間與對象原型。

core-js-compat

core-js-compat根據(jù)Browserslist維護了不同宿主環(huán)境、不同版本下對應需要支持特性的集合。

Browserslist[4]提供了不同瀏覽器、node版本下ES特性的支持情況

比如:

"browserslist": [
    "not IE 11",
    "maintained node versions"
  ]

代表:非IE11的版本以及所有Node.js基金會維護的版本。

@babel/polyfill與core-js關系

@babel/polyfill可以看作是:core-jsregenerator-runtime

regenerator-runtimegenerator以及async/await的運行時依賴

單獨使用@babel/polyfill會將core-js全量導入,造成項目打包體積過大。

從Babel v7.4.0[5]開始,@babel/polyfill被廢棄了,可以直接引用core-jsregenerator-runtime替代

為了解決全量引入core-js造成打包體積過大的問題,我們需要配合使用@babel/preset-env。

preset的含義

在介紹@babel/preset-env前,我們先來了解preset的意義。

初始情況下,Babel沒有任何額外能力,其工作流程可以描述為:

const babel = code => code;

其通過plugin對外提供介入babel-core的能力,類似webpackplugin對外提供介入webpack編譯流程的能力。

plugin分為幾類:

  • @babel/plugin-syntax-*語法相關插件,用于新的語法支持。比如babel-plugin-syntax-decorators[6]提供decorators的語法支持
  • @babel/plugin-proposal-*用于ES提案的特性支持,比如babel-plugin-proposal-optional-chaining可選鏈操作符特性支持
  • @babel/plugin-transform-*用于轉換代碼,transform插件內(nèi)部會使用對應syntax插件

多個plugin組合在一起形成的集合,被稱為preset。

@babel/preset-env

使用@babel/preset-env,可以「按需」將core-js中的特性打包,這樣可以顯著減少最終打包的體積。

這里的「按需」,分為兩個粒度:

  • 宿主環(huán)境的粒度。根據(jù)不同宿主環(huán)境將該環(huán)境下所需的所有特性打包
  • 按使用情況的粒度。僅僅將使用了的特性打包

我們來依次看下。

宿主環(huán)境的粒度

當我們按如下參數(shù)在項目目錄下配置browserslist文件(或在@babel/preset-envtargets屬性內(nèi)設置,或在package.jsonbrowserslist屬性中設置):

not IE 11
maintained node versions

會將「非IE11」且「所有Node.js基金會維護的node版本」下需要的特性打入最終的包。

顯然這是利用了剛才介紹的core-js這個Monorepo下的core-js-compat的能力。

按使用情況的粒度

更理想的情況是只打包我們使用過的特性。

這時候可以設置@babel/preset-envuseBuiltIns屬性為usage

比如:

a.js

var a = new Promise();

b.js

var b = new Map();

當宿主環(huán)境不支持promiseMap時,輸出的文件為:

a.js

import "core-js/modules/es.promise";
var a = new Promise();

b.js

import "core-js/modules/es.map";
var b = new Map();

當宿主環(huán)境支持這兩個特性時,輸出的文件為:

a.js

var a = new Promise();

b.js

var b = new Map();

進一步優(yōu)化打包體積

打開babel playground[7],輸入:

class App {}

會發(fā)現(xiàn)編譯出的結果為:

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為輔助方法。

如果多個文件都使用了class特性,那么每個文件打包對應的module中都將包含_classCallCheck。

為了減少打包體積,更好的方式是:需要使用「輔助方法」的module都從同一個地方引用,而不是自己維護一份。

@babel/runtime包含了Babel所有「輔助方法」以及regenerator-runtime

單純引入@babel/runtime還不行,因為Babel不知道何時引用@babel/runtime中的「輔助方法」。

所以,還需要引入@babel/plugin-transform-runtime。

這個插件會在編譯時將所有使用「輔助方法」的地方從「自己維護一份」改為從@babel/runtime中引入。

所以我們需要將@babel/plugin-transform-runtime置為devDependence,因為他在編譯時使用。

@babel/runtime置為dependence,因為他在運行時使用。

總結

本文從底層向上介紹了前端日常業(yè)務開發(fā)會接觸的Babel大家族成員。他們包括:

底層

@babel/core(由@babel/parser、@babel/traverse@babel/types、@babel/generator等組成)

他們提供了Babel編譯JS的能力。

注:這里@babel/core為庫名,前文中babel-core為其在倉庫中對應文件名

中層

@babel/plugin-*

Babel對外暴露的API,使開發(fā)者可以介入其編譯JS的能力

上層

@babel/preset-*

日常開發(fā)會使用的插件集合。

對于立志成為前端架構師的同學,Babel是前端工程化的基石,學懂、會用他是很有必要的。

能看到這里真不容易,給自己鼓鼓掌吧。

參考資料

[1]

Babel倉庫:?

[2]

AST explorer:?

[3]

core-js倉庫:?

[4]

Browserslist:?

[5]

Babel v7.4.0:?

[6]

babel-plugin-syntax-decorators:?

[7]

babel playground:?

發(fā)布于 2021-02-25 15:37

本文摘自 :https://www.cnblogs.com/

開通會員,享受整站包年服務立即開通 >