帶你在Babel的世界中暢游

前言
Babel在目前前端領(lǐng)域類似一座山一樣的存在,任何項目或多或少都有它的身影在浮現(xiàn)。

也許對于Babel絕大多數(shù)前端開發(fā)者都是處于一知半解的狀態(tài),但是無論是在實際業(yè)務(wù)開發(fā)中還是對于我們個人提升來說熟練掌握Babel一定是晉升高級前端工程師的必備之路。

文章中我們只講“干貨”,從原理出發(fā)結(jié)合深層次實踐帶你領(lǐng)略Babel之美。

我們會從Babel基礎(chǔ)內(nèi)容從而漸進(jìn)到Babel插件開發(fā)者的世界,從此讓你對于Babel得心應(yīng)手。

Babel日常用法
首先我們會從基礎(chǔ)的配置Babel及相關(guān)內(nèi)容開始講解。

常見plugin和Preset
首先我們來說說Plugin和Preset的區(qū)別和聯(lián)系。

所謂Preset就是一些Plugin組成的合集,你可以將Preset理解稱為就是一些的Plugin整合稱為的一個包。

常見Preset
文章中列舉了三個最常用的Preset,更多的Prest你可以在這里查閱。

babel-preset-env

@babel/preset-env是一個智能預(yù)設(shè),它可以將我們的高版本JavaScript代碼進(jìn)行轉(zhuǎn)譯根據(jù)內(nèi)置的規(guī)則轉(zhuǎn)譯成為低版本的javascript代碼。

preset-env內(nèi)部集成了絕大多數(shù)plugin(State > 3)的轉(zhuǎn)譯插件,它會根據(jù)對應(yīng)的參數(shù)進(jìn)行代碼轉(zhuǎn)譯。

@babel/preset-env不會包含任何低于 Stage 3 的 JavaScript 語法提案。如果需要兼容低于Stage 3階段的語法則需要額外引入對應(yīng)的Plugin進(jìn)行兼容。

需要額外注意的是babel-preset-env僅僅針對語法階段的轉(zhuǎn)譯,比如轉(zhuǎn)譯箭頭函數(shù),const/let語法。針對一些Api或者Es 6內(nèi)置模塊的polyfill,preset-env是無法進(jìn)行轉(zhuǎn)譯的。這塊內(nèi)容我們會在之后的polyfill中為大家進(jìn)行詳細(xì)講解。

babel-preset-react

通常我們在使用React中的jsx時,相信大家都明白實質(zhì)上jsx最終會被編譯稱為React.createElement()方法。

babel-preset-react這個預(yù)設(shè)起到的就是將jsx進(jìn)行轉(zhuǎn)譯的作用。

babel-preset-typescript

對于TypeScript代碼,我們有兩種方式去編譯TypeScript代碼成為JavaScript代碼。

使用tsc命令,結(jié)合cli命令行參數(shù)方式或者tsconfig配置文件進(jìn)行編譯ts代碼。

使用babel,通過babel-preset-typescript代碼進(jìn)行編譯ts代碼。

常見Plugin
Babel官網(wǎng)列舉出了一份非常詳盡的Plugin List。

關(guān)于常見的Plugin其實大多數(shù)都集成在了babel-preset-env中,當(dāng)你發(fā)現(xiàn)你的項目中并不能支持最新的js語法時,此時我們可以查閱對應(yīng)的Babel Plugin List找到對應(yīng)的語法插件添加進(jìn)入babel配置。

同時還有一些不常用的packages,比如@babel/register:它會改寫require命令,為它加上一個鉤子。此后,每當(dāng)使用require加載.js、.jsx、.es和.es6后綴名的文件,就會先用Babel進(jìn)行轉(zhuǎn)碼。

這些包日常中不是特別常用,如果有同學(xué)有相關(guān)編譯相關(guān)需求完全可以去babel官網(wǎng)查閱。如果官網(wǎng)不存在現(xiàn)成的plugin/package,別擔(dān)心!我們同時也會在之后手把手教大家babel插件的開發(fā)。

其中最常見的@babel/plugin-transform-runtime我們會在下面的Polyfill進(jìn)行詳細(xì)的講解。

前端基建中的Babel配置詳解
接下里我們聊聊前端項目構(gòu)建中相關(guān)的babel相關(guān)配置。

關(guān)于前端構(gòu)建工具,無路你使用的是webapack還是rollup又或是任何構(gòu)建打包工具,內(nèi)部都離不開Babel相關(guān)配置。

這里我們使用業(yè)務(wù)中最常用的webpack舉例,其他構(gòu)建工具在使用方面只是引入的包不同,Babel配置原理是相通的。

關(guān)于WebPack中我們?nèi)粘J褂玫腷abel相關(guān)配置主要涉及以下三個相關(guān)插件:

babel-loader

babel-core

babel-preset-env
也許你經(jīng)常在項目搭建過程中見到他們,這里我們將逐步使用一段偽代碼來講解他們之間的區(qū)別和聯(lián)系。

首先我們需要清楚在 webpack中l(wèi)oader的本質(zhì)就是一個函數(shù),接受我們的源代碼作為入?yún)⑼瑫r返回新的內(nèi)容。

babel-loader

所以babel-loader的本質(zhì)就是一個函數(shù),我們匹配到對應(yīng)的jsx?/tsx?的文件交給babel-loader:

/**
 *
 * @param sourceCode 源代碼內(nèi)容
 * @param options babel-loader相關(guān)參數(shù)
 * @returns 處理后的代碼
 */
function babelLoader (sourceCode,options) {
  // ..
  return targetCode
}
關(guān)于options,babel-loader支持直接通過loader的參數(shù)形式注入,同時也在loader函數(shù)內(nèi)部通過讀取.babelrc/babel.config.js/babel.config.json等文件注入配置。

babel-core

我們講到了babel-loader僅僅是識別匹配文件和接受對應(yīng)參數(shù)的函數(shù),那么babel在編譯代碼過程中核心的庫就是@babel/core這個庫。

babel-core是babel最核心的一個編譯庫,他可以將我們的代碼進(jìn)行詞法分析--語法分析--語義分析過程從而生成AST抽象語法樹,從而對于“這棵樹”的操作之后再通過編譯稱為新的代碼。

babel-core其實相當(dāng)于@babel/parse和@babel/generator這兩個包的合體,接觸過js編譯的同學(xué)可能有了解esprima和escodegen這兩個庫,你可以將babel-core的作用理解稱為這兩個庫的合體。

babel-core通過transform方法將我們的代碼進(jìn)行編譯。

關(guān)于babel-core中的編譯方法其實有很多種,比如直接接受字符串形式的transform方法或者接受js文件路徑的transformFile方法進(jìn)行文件整體編譯。

關(guān)于babel-core內(nèi)部的編譯使用規(guī)則,我們會在之后的插件章節(jié)中詳細(xì)講到。

接下來讓我們完善對應(yīng)的babel-loader函數(shù):

const core = require('@babel/core')

/**
 *
 * @param sourceCode 源代碼內(nèi)容
 * @param options babel-loader相關(guān)參數(shù)
 * @returns 處理后的代碼
 */
function babelLoader (sourceCode,options) {
  // 通過transform方法編譯傳入的源代碼
  core.transform(sourceCode)
  return targetCode
}
這里我們在babel-loader中調(diào)用了babel-core這個庫進(jìn)行了代碼的編譯作用。

babel-preset-env

上邊我們說到babel-loader本質(zhì)是一個函數(shù),它在內(nèi)部通過babel/core這個核心包進(jìn)行JavaScript代碼的轉(zhuǎn)譯。






但是針對代碼的轉(zhuǎn)譯我們需要告訴babel以什么樣的規(guī)則進(jìn)行轉(zhuǎn)化,比如我需要告訴babel:“嘿,babel。將我的這段代碼轉(zhuǎn)化稱為EcmaScript 5版本的內(nèi)容!”。

此時babel-preset-env在這里充當(dāng)?shù)木褪沁@個作用:告訴babel我需要以為什么樣的規(guī)則進(jìn)行代碼轉(zhuǎn)移。

const core = require('@babel/core');

/**
 *
 * @param sourceCode 源代碼內(nèi)容
 * @param options babel-loader相關(guān)參數(shù)
 * @returns 處理后的代碼
 */
function babelLoader(sourceCode, options) {
  // 通過transform方法編譯傳入的源代碼
  core.transform(sourceCode, {
    presets: ['babel-preset-env'],
    plugins: [...]
  });
  return targetCode;
}
這里plugin和prest其實是同一個東西,所以我將plugin直接放在代碼中了。同理一些其他的preset或者plugin也是發(fā)揮這樣的作用。

關(guān)于babel的基礎(chǔ)基建配置我相信講到這里大家已經(jīng)明白了他們對應(yīng)的職責(zé)和基礎(chǔ)原理.


Babel相關(guān)polyfill內(nèi)容
何謂polyfill
關(guān)于polyfill,我們先來解釋下何謂polyfill。

首先我們來理清楚這三個概念:

最新ES語法,比如:箭頭函數(shù),let/const。
最新ES Api,比如Promise
最新ES實例/靜態(tài)方法,比如String.prototype.include
babel-prest-env僅僅只會轉(zhuǎn)化最新的es語法,并不會轉(zhuǎn)化對應(yīng)的Api和實例方法,比如說ES 6中的Array.from靜態(tài)方法。babel是不會轉(zhuǎn)譯這個方法的,如果想在低版本瀏覽器中識別并且運行Array.from方法達(dá)到我們的預(yù)期就需要額外引入polyfill進(jìn)行在Array上添加實現(xiàn)這個方法。

其實可以稍微簡單總結(jié)一下,語法層面的轉(zhuǎn)化preset-env完全可以勝任。但是一些內(nèi)置方法模塊,僅僅通過preset-env的語法轉(zhuǎn)化是無法進(jìn)行識別轉(zhuǎn)化的,所以就需要一系列類似”墊片“的工具進(jìn)行補(bǔ)充實現(xiàn)這部分內(nèi)容的低版本代碼實現(xiàn)。這就是所謂的polyfill的作用,

針對于polyfill方法的內(nèi)容,babel中涉及兩個方面來解決:

@babel/polyfill

@babel/runtime

@babel/plugin-transform-runtime

我們理清了何謂polyfill以及polyfill的作用和含義后,讓我們來逐個擊破這兩個babel包對應(yīng)的使用方式和區(qū)別吧。

@babel/polyfill

首先我們來看看第一種實現(xiàn)polyfill的方式:

@babel/polyfill介紹

通過babelPolyfill通過往全局對象上添加屬性以及直接修改內(nèi)置對象的Prototype上添加方法實現(xiàn)polyfill。

比如說我們需要支持String.prototype.include,在引入babelPolyfill這個包之后,它會在全局String的原型對象上添加include方法從而支持我們的Js Api。

我們說到這種方式本質(zhì)上是往全局對象/內(nèi)置對象上掛載屬性,所以這種方式難免會造成全局污染。

應(yīng)用@babel/polyfill

在babel-preset-env中存在一個useBuiltIns參數(shù),這個參數(shù)決定了如何在preset-env中使用@babel/polyfill。

{
    "presets": [
        ["@babel/preset-env", {
            "useBuiltIns": false
        }]
    ]
}
useBuiltIns--"usage"| "entry"| false
false

當(dāng)我們使用preset-env傳入useBuiltIns參數(shù)時候,默認(rèn)為false。它表示僅僅會轉(zhuǎn)化最新的ES語法,并不會轉(zhuǎn)化任何Api和方法。

entry

當(dāng)傳入entry時,需要我們在項目入口文件中手動引入一次core-js,它會根據(jù)我們配置的瀏覽器兼容性列表(browserList)然后全量引入不兼容的polyfill。

Tips:  在Babel7.4。0之后,@babel/polyfill被廢棄它變成另外兩個包的集成。"core-js/stable"; "regenerator-runtime/runtime";。你可以在這里看到變化,但是他們的使用方式是一致的,只是在入口文件中引入的包不同了。

// 項目入口文件中需要額外引入polyfill
// core-js 2.0中是使用"@babel/polyfill" core-js3.0版本中變化成為了上邊兩個包
import "@babel/polyfill"

// babel
{
    "presets": [
        ["@babel/preset-env", {
            "useBuiltIns": "entry"
        }]
    ]
}
同時需要注意的是,在我們使用useBuiltIns:entry/usage時,需要額外指定core-js這個參數(shù)。默認(rèn)為使用core-js 2.0,所謂的core-js就是我們上文講到的“墊片”的實現(xiàn)。它會實現(xiàn)一系列內(nèi)置方法或者Promise等Api。

core-js 2.0版本是跟隨preset-env一起安裝的,不需要單獨安裝哦~

usage

上邊我們說到配置為entry時,perset-env會基于我們的瀏覽器兼容列表進(jìn)行全量引入polyfill。所謂的全量引入比如說我們代碼中僅僅使用了Array.from這個方法。但是polyfill并不僅僅會引入Array.from,同時也會引入Promise、Array.prototype.include等其他并未使用到的方法。這就會造成包中引入的體積太大了。

此時就引入出了我們的useBuintIns:usage配置。

當(dāng)我們配置useBuintIns:usage時,會根據(jù)配置的瀏覽器兼容,以及代碼中 使用到的Api 進(jìn)行引入polyfill按需添加。

當(dāng)使用usage時,我們不需要額外在項目入口中引入polyfill了,它會根據(jù)我們項目中使用到的進(jìn)行按需引入。






{
    "presets": [
        ["@babel/preset-env", {
            "useBuiltIns": "usage",
            "core-js": 3
        }]
    ]
}
關(guān)于usage和entry存在一個需要注意的本質(zhì)上的區(qū)別。

我們以項目中引入Promise為例。

當(dāng)我們配置useBuintInts:entry時,僅僅會在入口文件全量引入一次polyfill。你可以這樣理解:

// 當(dāng)使用entry配置時
...
// 一系列實現(xiàn)polyfill的方法
global.Promise = promise

// 其他文件使用時
const a = new Promise()
而當(dāng)我們使用useBuintIns:usage時,preset-env只能基于各個模塊去分析它們使用到的polyfill從而進(jìn)入引入。

preset-env會幫助我們智能化的在需要的地方引入,比如:

// a. js 中
import "core-js/modules/es.promise";

...
// b.js中

import "core-js/modules/es.promise";
...
在usage情況下,如果我們存在很多個模塊,那么無疑會多出很多冗余代碼(import語法)。

同樣在使用usage時因為是模塊內(nèi)部局部引入polyfill所以按需在模塊內(nèi)進(jìn)行引入,而entry則會在代碼入口中一次性引入。

usageBuintIns不同參數(shù)分別有不同場景的適應(yīng)度,具體參數(shù)使用場景還需要大家結(jié)合自己的項目實際情況找到最佳方式。

@babel/runtime

上邊我們講到@babel/polyfill是存在污染全局變量的副作用,在實現(xiàn)polyfill時Babel還提供了另外一種方式去讓我們實現(xiàn)這功能,那就是@babel/runtime。

簡單來講,@babel/runtime更像是一種按需加載的解決方案,比如哪里需要使用到Promise,@babel/runtime就會在他的文件頂部添加import promise from 'babel-runtime/core-js/promise'。

同時上邊我們講到對于preset-env的useBuintIns配置項,我們的polyfill是preset-env幫我們智能引入。

而babel-runtime則會將引入方式由智能完全交由我們自己,我們需要什么自己引入什么。

它的用法很簡單,只要我們?nèi)グ惭bnpm install --save @babel/runtime后,在需要使用對應(yīng)的polyfill的地方去單獨引入就可以了。比如:

// a.js 中需要使用Promise 我們需要手動引入對應(yīng)的運行時polyfill

import Promise from 'babel-runtime/core-js/promise'

const promsies = new Promise()
總而言之,babel/runtime你可以理解稱為就是一個運行時“哪里需要引哪里”的工具庫。

針對babel/runtime絕大多數(shù)情況下我們都會配合@babel/plugin-transfrom-runtime進(jìn)行使用達(dá)到智能化runtime的polyfill引入。

@babel/plugin-transform-runtime

babel-runtime存在的問題

babel-runtime在我們手動引入一些polyfill的時候,它會給我們的代碼中注入一些類似_extend(), classCallCheck()之類的工具函數(shù),這些工具函數(shù)的代碼會包含在編譯后的每個文件中,比如:

class Circle {}
// babel-runtime 編譯Class需要借助_classCallCheck這個工具函數(shù)
function _classCallCheck(instance, Constructor) { //... }
var Circle = function Circle() { _classCallCheck(this, Circle); };
如果我們項目中存在多個文件使用了class,那么無疑在每個文件中注入這樣一段冗余重復(fù)的工具函數(shù)將是一種災(zāi)難。

所以針對上述提到的兩個問題:

babel-runtime無法做到智能化分析,需要我們手動引入。
babel-runtime編譯過程中會重復(fù)生成冗余代碼。
我們就要引入我們的主角@babel/plugin-transform-runtime。

@babel/plugin-transform-runtime作用
@babel/plugin-transform-runtime插件的作用恰恰就是為了解決上述我們提到的run-time存在的問題而提出的插件。

babel-runtime無法做到智能化分析,需要我們手動引入。

@babel/plugin-transform-runtime插件會智能化的分析我們的項目中所使用到需要轉(zhuǎn)譯的js代碼,從而實現(xiàn)模塊化從babel-runtime中引入所需的polyfill實現(xiàn)。

babel-runtime編譯過程中會重復(fù)生成冗余代碼。

@babel/plugin-transform-runtime插件提供了一個helpers參數(shù)。具體你可以在這里查閱它的所有配置參數(shù)。

這個helpers參數(shù)開啟后可以將上邊提到編譯階段重復(fù)的工具函數(shù),比如classCallCheck, extends等代碼轉(zhuǎn)化稱為require語句。此時,這些工具函數(shù)就不會重復(fù)的出現(xiàn)在使用中的模塊中了。比如這樣:

// @babel/plugin-transform-runtime會將工具函數(shù)轉(zhuǎn)化為require語句進(jìn)行引入

// 而非runtime那樣直接將工具模塊代碼注入到模塊中
var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");
var Circle = function Circle() { _classCallCheck(this, Circle); };
配置@babel/plugin-transform-runtime
其實用法原理部分已經(jīng)在上邊分析的比較透徹了,配置這里還有疑問的同學(xué)可以評論區(qū)給我留言或者移步babel官網(wǎng)查看。

這里為列一份目前它的默認(rèn)配置:

{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "absoluteRuntime": false,
        "corejs": false,
        "helpers": true,
        "regenerator": true,
        "version": "7.0.0-beta.0"
      }
    ]
  ]
}
總結(jié)polyfill
我們可以看到針對polyfill其實我耗費了不少去將它們之間的區(qū)別和聯(lián)系,讓我們來稍微總結(jié)一下吧。

在babel中實現(xiàn)polyfill主要有兩種方式:

一種是通過@babel/polyfill配合preset-env去使用,這種方式可能會存在污染全局作用域。

一種是通過@babel/runtime配合@babel/plugin-transform-runtime去使用,這種方式并不會污染作用域。

全局引入會污染全局作用域,但是相對于局部引入來說。它會增加很多額外的引入語句,增加包體積。

在useBuintIns:usage情況下其實和@babel/plugin-transform-runtime情況下是類似的作用,

通常我個人選擇是會在開發(fā)類庫時遵守不污染全局為首先使用@babel/plugin-transform-runtime而在業(yè)務(wù)開發(fā)中使用@babel/polyfill。

babel-runtime 是為了減少重復(fù)代碼而生的。babel生成的代碼,可能會用到一些_extend(), classCallCheck() 之類的工具函數(shù),默認(rèn)情況下,這些工具函數(shù)的代碼會包含在編譯后的文件中。如果存在多個文件,那每個文件都有可能含有一份重復(fù)的代碼。

babel-runtime插件能夠?qū)⑦@些工具函數(shù)的代碼轉(zhuǎn)換成require語句,指向為對babel-runtime的引用,如 require('babel-runtime/helpers/classCallCheck'). 這樣, classCallCheck的代碼就不需要在每個文件中都存在了。

作者:19組清風(fēng) 鏈接:https://juejin.cn/post/7025237833543581732

作者:19組清風(fēng)


歡迎關(guān)注微信公眾號 :前端陽光