一文搞懂Babel配置

語言: CN / TW / HK

theme: smartblue

最近在做一次Babel6升級Babel7的操作,把升級的過程和關於babel的配置進行一次總結。

1 為什麼講Babel配置

Babel 是一個工具鏈,主要用於將 ECMAScript 2015+ 版本的程式碼轉換為向後相容的 JavaScript 語法,以便能夠執行在當前和舊版本的瀏覽器或其他環境中。

其實目前前端開發,各種專案模版,你也不需要關心babel的配置,隨便拉下來一個就能執行,但是要做定製化的處理還是要把babel搞懂。

@babel/cli是Babel的命令列工具,我們一般用不到,因為我們通常都是用babel-loader,裡邊使用的是@babel/core的api形式,我們只需要關心Babel的配置,如果有需要在編譯階段對程式碼進行處理 也可以寫自己的外掛,但是大部分場景是需要我們把Babel的配置搞清楚。

2 Babel的配置檔案

Babel6的階段 最常用的是.babelrc,但是現在Babel7支援了更多格式:

const RELATIVE_CONFIG_FILENAMES = [".babelrc", ".babelrc.js", ".babelrc.cjs", ".babelrc.mjs", ".babelrc.json"];package.json files with a "babel" key。

配置檔案的格式如下: { "presets": [ [ "@babel/preset-env", { "modules": "commonjs" } ] ], "plugins": [ [ "@babel/plugin-transform-runtime", { "corejs": 3 } ], "@babel/plugin-syntax-dynamic-import", ] } } 更詳細介紹參見Babel Config

2.1 pluginspreset

配置檔案中主要有兩個配置pluginspreset,@babel/core本身對程式碼不做任何的轉化,但是提供了對程式碼的解析,各種外掛在解析過程中可以進行程式碼的轉換,比如處理箭頭函式的外掛@babel/plugin-transform-arrow-functions等等,所以比如針對ES6語法的解析就需要很多外掛,preset預設就是配置的子集,預設的一套配置,可以根據引數動態的返回配置。

2.2 執行順序

順序問題很重要,比如某一個外掛是新增'use strict', 一個外掛是刪除'use strict',如果想要刪除成功,就要保證執行順序。 在一個配置裡面 * 外掛在 presets 前執行。 * 外掛順序從前往後排列。 * preset 順序是顛倒的(從後往前)。 所以在preset中的外掛,肯定比外層的外掛要後執行。

2.3 傳引數

pluginspreset的配置是陣列的形式,如果不需要傳引數,最基本的就是字串名稱,如果需要傳引數,把它寫成陣列的形式,陣列第一項是字串名稱,第二項是要傳的引數物件。

3 Babel的升級

3.1 廢棄的preset

@babel/preset-env已經完全可以替換 * babel-preset-es2015 * babel-preset-es2016 * babel-preset-es2017 * babel-preset-latest

所有stage的preset在Babel v7.0.0-beta.55版本都已經被廢棄了, stage-x:指處於某一階段的js語言提案

  • Stage 0 - 設想(Strawman):只是一個想法,可能有 Babel外掛。
  • Stage 1 - 建議(Proposal):這是值得跟進的。
  • Stage 2 - 草案(Draft):初始規範。
  • Stage 3 - 候選(Candidate):完成規範並在瀏覽器上初步實現。
  • Stage 4 - 完成(Finished):將新增到下一個年度版本釋出中。

最開始stage的出現是為了方便開發人員,每個階段的外掛與TC39和社群相互作用,同步更新,使用者可以直接引用對應stage支援的語法特性。關於廢棄的原因 總結下來是: * 1 對使用者太黑盒了,當提案發生重大變化和廢棄時,stage內部的外掛就會變化,使用者可能會出現未編譯的語法。 * 2 當用戶想要支援某種語法時,不知道在某一個stage裡,所以最好是讓使用者自己去新增外掛,或者你只需要指定瀏覽器的相容性,preset中動態的新增對應外掛。 * 3 第三點舉了個例子,很多人都把裝飾器特性加做ES7,其實這只是階段0的實驗性建議,可能永遠不會成為JS的一部分。不要將其稱為“ES7”,我們要時刻提醒開發者babel是怎麼工作的。

3.1 廢棄的polyfill

先說下已經有了Babel為什麼還要polyfill,Babel預設只轉換新的JavaScript句法(syntax),而不轉換新的API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全域性物件,以及一些定義在全域性物件上的方法(比如Object.assign)都不會轉碼。舉個栗子,ES6在Array物件上新增了Array.from方法。babel就不會轉碼這個方法。所以之前我們都需要引入polyfill。

但是從Babel 7.4.0開始,不推薦使用此軟體包,而直接包括core-js/stable(包括regenerator-runtime/runtimepolyfill ECMAScript功能)和(需要使用轉譯的生成器函式)。 import "core-js/stable"; import "regenerator-runtime/runtime"; 但是最優的方式也不是直接這樣引入,後面講@babel/preset-env的使用時會有更好的方式。

3.3 babel-upgrade

關於升級,官方提供了工具 babel-upgrade 總結關鍵點如下: * 1 node版本8以上 這個應該都不是問題了。 * 2 npx babel-upgrade --write --install,兩個引數,--write會把更新的配置寫入babel的配置檔案中,package.json中也會更新依賴,但是發現沒有的依賴沒有新增,所以我在更新的時候把配置中依賴的npm包,在package.json都check了一遍。--install是會進行一次安裝操作。

4 @babel/preset-env

@babel/preset-env是Babel推薦的最智慧的預設,在使用了 babel-upgrade 升級之後你就可以看到配置中會有這個預設,因為設個預設集成了常用外掛和polyfill能力,可以根據使用者指定的環境尋找對應的外掛。

下面對它的關鍵配置項做說明。

4.1 target

string | Array<string> | { [string]: string },預設為{}

描述您為專案支援/目標的環境。

這可以是與瀏覽器列表相容的查詢:

```json { "targets": "> 0.25%, not dead" }

``` 或支援最低環境版本的物件:

```json { "targets": { "chrome": "58", "ie": "11" } }

`` 實施例的環境中:chromeoperaedgefirefoxsafariieiosandroidnodeelectron`。

如果未指定目標,則旁註@babel/preset-env將預設轉換所有ECMAScript 2015+程式碼,所以不建議。

4.2 useBuiltIns

"usage"| "entry"| false,預設為false

此選項決定@babel/preset-env如何處理polyfill的引入。

前面將廢棄polyfill時 講到了polyfill現在分為兩個npm包,是這樣引入 import "core-js/stable"; import "regenerator-runtime/runtime"; 但是問題是全量引入,增加包體積,所以useBuiltIns選項就是對其進行優化。

當取值"entry"時,@babel/preset-env 會把全量引入替換為目標環境特定需要的模組。

當目標瀏覽器是 chrome 72 時,上面的內容將被 @babel/preset-env 轉換為

require("core-js/modules/es.array.reduce"); require("core-js/modules/es.array.reduce-right"); require("core-js/modules/es.array.unscopables.flat"); require("core-js/modules/es.array.unscopables.flat-map"); require("core-js/modules/es.math.hypot"); require("core-js/modules/es.object.from-entries"); require("core-js/modules/web.immediate");

當取值"usage"時,我們無需手動引入polyfill檔案,@babel/preset-env 在每個檔案的開頭引入目標環境不支援、僅在當前檔案中使用的 polyfills。

例如, const set = new Set([1, 2, 3]); [1, 2, 3].includes(2); 當目標環境是老的瀏覽器例如 ie 11,將轉換為 ``` import "core-js/modules/es.array.includes"; import "core-js/modules/es.array.iterator"; import "core-js/modules/es.object.to-string"; import "core-js/modules/es.set";

const set = new Set([1, 2, 3]); [1, 2, 3].includes(2); 當目標是 `chrome 72` 時不需要匯入,因為這個環境不需要 polyfills: const set = new Set([1, 2, 3]); [1, 2, 3].includes(2); ```

4.3 core-js

core-js就是Javascript標準庫的polyfill,@babel/preset-env的polyfill就依賴於它,所以我們需要指定使用的core-js的版本,目前最新版本是3。 預設情況下,僅注入穩定ECMAScript功能的polyfill,如果想使用一些提案的語法,可以有三種選擇: * 使用useBuiltIns: "entry"時,可以直接匯入建議填充工具import "core-js/proposals/string-replace-all"。 * 使用useBuiltIns: "usage"時,您有兩種不同的選擇: * 將shippedProposals選項設定為true。這將啟用已經在瀏覽器中釋出一段時間的投標的polyfill和transforms。 * 使用corejs: { version: 3, proposals: true }。這樣可以對所支援的每個提案進行填充core-js

4.4 exclude

我覺得這個選擇有用,因為@babel/preset-env中內建的外掛,我們無法在其後執行,比如裡面內建的"@babel/plugin-transform-modules-commonjs"外掛會預設的在所有的模組上都新增use strict 嚴格模式, 雖然有babel-plugin-remove-use-strict用於移除use strict 但是由於執行順序的問題,還是無法移除。 第二個問題就是內建外掛無法傳引數的問題。 所以我想到的方法是先exclude排除掉這個外掛,然後在外層再新增 這樣就可以改變執行順序同時也可以自定義傳引數。

5 @babel/plugin-transform-runtime

已經有了polyfill,這個包的作用是什麼?主要分兩類:

  • 1 減少程式碼體積,Babel的編譯會在每一個模組都新增一些行內的程式碼墊片,例如await_asyncToGeneratorasyncGeneratorStep,使用了它之後會把這些方法通過@babel/runtime/helpers中的模組進行替換。

例如程式碼 async function a () { await new Promise(function(resolve, reject) { resolve(1) }) } 沒使用之前,編譯結果

``` require("regenerator-runtime/runtime");

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = genkey; var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

function _a() { _a = _asyncToGenerator( /#PURE/regeneratorRuntime.mark(function _callee() { return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return new Promise(function (resolve, reject) { resolve(1); });

      case 2:
      case "end":
        return _context.stop();
    }
  }
}, _callee);

})); return _a.apply(this, arguments); } ```

使用之後 ```

var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

require("regenerator-runtime/runtime");

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));

```

  • 2 區域性引入 不影響全域性變數

@babel/preset-env中引入的polyfill都是直接引入的core-js下的模組,它的問題會汙染全域性變數,比如

"foobar".includes("foo");

編譯後的polyfill是給String.prototype添加了includes方法,所以會影響全域性的String物件。

require("core-js/modules/es.string.includes");

而使用了@babel/plugin-transform-runtime後的編譯結果

``` var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));

(0, _includes.default)(_context = "foobar").call(_context, "foo"); ```

會把程式碼中用的到的方法進行包裝,而不會對全域性變數產生影響。

最後是 @babel/plugin-transform-runtime的配置項,關鍵的是指定 core-js的版本。

corejs: 2僅支援全域性變數(例如Promise)和靜態屬性(例如Array.from),corejs: 3還支援例項屬性(例如[].includes)。

預設情況下,@babel/plugin-transform-runtime不填充提案。如果您使用corejs: 3,則可以通過使用proposals: true選項啟用此功能。

需要安裝對應的執行時依賴: npm install --save @babel/runtime-corejs3

總結

本文講解了Babel配置中的一些細節問題,基於以上知識你就可以打造符合自己團隊開發需求的的preset

  • 如果覺得有用請幫忙點個贊🙏。
  • 我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿