史上最清晰易懂的babel配置解析

菜菜_張發表於2019-03-31

標題黨了哈哈哈~~~

原文地址

相信很多人和筆者從前一樣,babel的配置都是從網上覆制黏貼或者使用現成的腳手架,雖然這能夠工作但還是希望大家能夠知其所以然,因此本文將對babel(babel@7)的配置做一次較為完整的梳理。

語法和api

es6增加的內容可以分為語法和api兩部分,搞清楚這點很重要,新語法比如箭頭函式、解構等:

const fn = () => {}

const arr2 = [...arr1]
複製程式碼

新的api比如Map、Promise等:

const m = new Map()

const p = new Promise(() => {})
複製程式碼

@babel/core

@babel/core,看名字就知道這是babel的核心,沒他不行,所以首先安裝這個包

npm install @babel/core
複製程式碼

它的作用就是根據我們的配置檔案轉換程式碼,配置檔案通常為.babelrc(靜態檔案)或者babel.config.js(可程式設計),這裡以.babelrc為例,在專案的根目錄下建立一個空檔案命名為.babelrc,然後建立一個js檔案(test.js)測試用:

/* test.js */
const fn = () => {}
複製程式碼

這裡我們安裝下@babel/cli以便能夠在命令列使用babel

npm install @babel/cli
複製程式碼

安裝完成後執行babel編譯,命令列輸入

npx babel test.js --watch --out-file test-compiled.js
複製程式碼

結果發現test-compiled.js的內容依然是es6的箭頭函式,不用著急,我們的.babelrc還沒有寫配置呢

Plugins和Presets

Now, out of the box Babel doesn't do anything. It basically acts like const babel = code => code; by parsing the code and then generating the same code back out again. You will need to add plugins for Babel to do anything.

上面是babel官網的一段話,可以理解為babel是基於外掛架構的,假如你什麼外掛也不提供,那麼babel什麼也不會做,即你輸入什麼輸出的依然是什麼。那麼我們現在想要把剪頭函式轉換為es5函式只需要提供一個箭頭函式外掛就可以了:

/* .babelrc */
{
  "plugins": ["@babel/plugin-transform-arrow-functions"]    
}
複製程式碼

轉換後的test-compiled.js為:

/* test.js */
const fn = () => {}

/* test-compiled.js */
const fn = function () {}
複製程式碼

那我想使用es6的解構語法怎麼辦?很簡單,新增解構外掛就行了:

/* .babelrc */
{
  "plugins": [
    "@babel/plugin-transform-arrow-functions",
    "@babel/plugin-transform-destructuring"
  ]    
}
複製程式碼

問題是有那麼多的語法需要轉換,一個個的新增外掛也太麻煩了,幸好babel提供了presets,他可以理解為外掛的集合,省去了我們一個個引入外掛的麻煩,官方提供了很多presets,比如preset-env(處理es6+規範語法的外掛集合)、preset-stage(處理尚處在提案語法的外掛集合)、preset-react(處理react語法的外掛集合)等,這裡我們主要介紹下preset-env

/* .babelrc */
{
  "presets": ["@babel/preset-env"]    
}
複製程式碼

preset-env

@babel/preset-env is a smart preset that allows you to use the latest JavaScript without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s).

以上是babel官網對preset-env的介紹,大致意思是說preset-env可以讓你使用es6的語法去寫程式碼,並且只轉換需要轉換的程式碼。預設情況下preset-env什麼都不需要配置,此時他轉換所有es6+的程式碼,然而我們可以提供一個targets配置項指定執行環境:

/* .babelrc */
{
  "presets": [
    ["@babel/preset-env", {
      "targets": "ie >= 8"
    }]
  ]    
}
複製程式碼

此時只有ie8以上版本瀏覽器不支援的語法才會被轉換,檢視我們的test-compiled.js檔案發現一切都很好:

/* test.js */
const fn = () => {}
const arr1 = [1, 2, 3]
const arr2 = [...arr1]


/* test-compiled.js */
var fn = function fn() {};
var arr1 = [1, 2, 3];
var arr2 = [].concat(arr1);
複製程式碼

@babel/polyfill

現在我們稍微改一下test.js:

/* test.js */
const fn = () => {}
new Promise(() => {})


/* test-compiled.js */
var fn = function fn() {};
new Promise(function () {});
複製程式碼

我們發現Promise並沒有被轉換,什麼!ie8還支援Promise?那是不可能的...。還記得本文開頭提到es6+規範增加的內容包括新的語法和新的api,新增的語法是可以用babel來transform的,但是新的api只能被polyfill,因此需要我們安裝@babel/polyfill,再簡單的修改下test.js如下:

/* test.js */
import '@babel/polyfill'

const fn = () => {}
new Promise(() => {})


/* test-compiled.js */
import '@babel/polyfill';

var fn = function fn() {};
new Promise(function () {});
複製程式碼

現在程式碼可以完美的執行在ie8的環境了,但是還存在一個問題:@babel/polyfill這個包的體積太大了,我們只需要Promise就夠了,假如能夠按需polyfill就好了。真巧,preset-env剛好提供了這個功能:

/* .babelrc */
{
  "presets": [
    ["@babel/preset-env", {
      "modules": false,
      "useBuiltIns": "entry",
      "targets": "ie >= 8"
    }]
  ]    
}
複製程式碼

我們只需給preset-env新增一個useBuiltIns配置項即可,值可以是entryusage,假如是entry,會在入口處把所有ie8以上瀏覽器不支援api的polyfill引入進來,如下:

/* test.js */
import '@babel/polyfill'

const fn = () => {}
new Promise(() => {})


/* test-compiled.js */
import "core-js/modules/es6.array.copy-within";
import "core-js/modules/es6.array.every";
import "core-js/modules/es6.array.fill";
...   //省略若干引入
import "core-js/modules/web.immediate";
import "core-js/modules/web.dom.iterable";
import "regenerator-runtime/runtime";

var fn = function fn() {};
new Promise(function () {});
複製程式碼

細心的你會發現transform後,import '@babel/polyfill'消失了,反倒是多了一堆import 'core-js/...'的內容,事實上,@babel/polyfill這個包本身是沒有內容的,它依賴於core-jsregenerator-runtime這兩個包,這兩個包提供了es6+規範的執行時環境。因此當我們不需要按需polyfill時直接引入@babel-polyfill就行了,它會把core-jsregenerator-runtime全部匯入,當我們需要按需polyfill時只需配置下useBuiltIns就行了,它會根據目標環境自動按需引入core-jsregenerator-runtime

前面還提到useBuiltIns的值還可以是usage,其功能更為強大,它會掃描你的程式碼,只有你的程式碼用到了哪個新的api,它才會引入相應的polyfill:

/* .babelrc */
{
  "presets": [
    ["@babel/preset-env", {
      "modules": false,
      "useBuiltIns": "usage",
      "targets": "ie >= 8"
    }]
  ]    
}
複製程式碼

transform後的test-compiled.js相應的會簡化很多:

/* test.js */
const fn = () => {}
new Promise(() => {})

/* test-compiled.js */
import "core-js/modules/es6.promise";
import "core-js/modules/es6.object.to-string";

var fn = function fn() {};
new Promise(function () {});
複製程式碼

遺憾的是這個功能還處於試驗狀態,謹慎使用。

事實上假如你是在寫一個app的話,以上關於babel的配置差不多已經夠了,你可能需要新增一些特定用途的PluginPreset,比如react專案你需要在presets新增@babel/preset-react,假如你想使用動態匯入功能你需要在plugins新增@babel/plugin-syntax-dynamic-import等等,這些不在贅述。假如你是在寫一個公共的庫或者框架,下面提到的點可能還需要你注意下。

@babel/runtime

有時候語法的轉換相對複雜,可能需要一些helper函式,如轉換es6的class:

/* test.js */
class Test {}


/* test-compiled.js */
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Test = function Test() {
  _classCallCheck(this, Test);
};
複製程式碼

示例中es6的class需要一個_classCallCheck輔助函式,試想假如我們多個檔案中都用到了es6的class,那麼每個檔案都需要定義一遍_classCallCheck函式,這也是一筆不小的浪費,假如將這些helper函式抽離到一個包中,由所有的檔案共同引用則可以減少可觀的程式碼量。而@babel/runtime做的正好是這件事,它提供了各種各樣的helper函式,但是我們如何知道該引入哪一個helper函式呢?總不能自己手動引入吧,事實上babel提供了一個@babel/plugin-transform-runtime外掛幫我們自動引入helper。我們首先安裝@babel/runtime@babel/plugin-transform-runtime

npm install @babel/runtime @babel/plugin-transform-runtime
複製程式碼

然後修改babel配置如下:

/* .babelrc */
{
  "presets": [
    ["@babel/preset-env", {
      "modules": false,
      "useBuiltIns": "usage",
      "targets": "ie >= 8"
    }]
  ],
  "plugins": [
    "@babel/plugin-transform-runtime"
  ]  
}
複製程式碼

現在我們再來看test-compiled.js檔案,裡面的_classCallCheck輔助函式已經是從@babel/runtime引入的了:

/* test.js */
class Test {}


/* test-compiled.js */
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";

var Test = function Test() {
  _classCallCheck(this, Test);
};
複製程式碼

看到這裡你可能會說,這不扯淡嘛!幾個helper函式能為我減少多少體積,我才懶得安裝外掛。事實上@babel/plugin-transform-runtime還有一個更重要的功能,它可以為你的程式碼建立一個sandboxed environment(沙箱環境),這在你編寫一些類庫等公共程式碼的時候尤其重要。

上文我們提到,對於Promise、Map等這些es6+規範的api我們是通過提供polyfill相容低版本瀏覽器的,這樣做會有一個副作用就是汙染了全域性變數,假如你是在寫一個app還好,但如果你是在寫一個公共的類庫可能會導致一些問題,你的類庫可能會把一些全域性的api覆蓋掉。幸好@babel/plugin-transform-runtime給我們提供了一個配置項corejs,它可以將這些變數隔離在區域性作用域中:

/* .babelrc */

{
  "presets": [
    ["@babel/preset-env", {
      "modules": false,
      "targets": "ie >= 8"
    }]
  ],
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "corejs": 2
    }]
  ]  
}
複製程式碼

注意:這裡一定要配置corejs,同時安裝@babel/runtime-corejs2,不配置的情況下@babel/plugin-transform-runtime預設是不引入這些polyfill的helper的。corejs的值現階段一般指定為2,可以近似理解為是@babel/runtime的版本。我們現在再來看下test-compiled.js被轉換成了什麼:

/* test.js */
class Test {}
new Promise(() => {})


/* test-compiled.js */
import _Promise from "@babel/runtime-corejs2/core-js/promise";
import _classCallCheck from "@babel/runtime-corejs2/helpers/classCallCheck";

var Test = function Test() {
  _classCallCheck(this, Test);
};

new _Promise(function () {});
複製程式碼

如我們所願,已經為Promise的polyfill建立了一個沙箱環境。

最後我們再為test.js稍微新增點內容:

/*  test.js */
class Test {}
new Promise(() => {})

const b = [1, 2, 3].includes(1)


/* test-compiled.js */
import _Promise from "@babel/runtime-corejs2/core-js/promise";
import _classCallCheck from "@babel/runtime-corejs2/helpers/classCallCheck";

var Test = function Test() {
  _classCallCheck(this, Test);
};

new _Promise(function () {});
var b = [1, 2, 3].includes(1);
複製程式碼

可以發現,includes方法並沒有引入輔助函式,可這明明也是es6裡面的api啊。這是因為includes是陣列的例項方法,要想polyfill必須修改Array的原型,這樣一來就汙染了全域性環境,因此@babel/plugin-transform-runtime是處理不了這些es6+規範的例項方法的。

tips

以上基本是本文的全部內容了,最後再來個總結和需要注意的地方:

  1. 本文沒有提到preset-stage,事實上babel@7已經不推薦使用它了,假如你需要使用尚在提案的語法,請直接新增相應的plugin。
  2. 對於普通專案,可以直接使用preset-env配置polyfill
  3. 對於類庫專案,推薦使用@babel/runtime,需要注意一些例項方法的使用
  4. 本文內容是基於babel@7,專案中遇到問題可以嘗試更新下babel-loader的版本 ...待補充

全文完

相關文章