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

標題黨了哈哈哈~~~react

原文地址git

相信不少人和筆者從前同樣,babel的配置都是從網上覆制黏貼或者使用現成的腳手架,雖然這可以工做但仍是但願你們可以知其因此然,所以本文將對babel(babel@7)的配置作一次較爲完整的梳理。es6

語法和api

es6增長的內容能夠分爲語法和api兩部分,搞清楚這點很重要,新語法好比箭頭函數、解構等:github

const fn = () => {}

const arr2 = [...arr1]
複製代碼

新的api好比Map、Promise等:web

const m = new Map()

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

@babel/core

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

npm install @babel/core
複製代碼

它的做用就是根據咱們的配置文件轉換代碼,配置文件一般爲.babelrc(靜態文件)或者babel.config.js(可編程),這裏以.babelrc爲例,在項目的根目錄下建立一個空文件命名爲.babelrc,而後建立一個js文件(test.js)測試用:npm

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

這裏咱們安裝下@babel/cli以便可以在命令行使用babel編程

npm install @babel/cli
複製代碼

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

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

結果發現test-compiled.js的內容依然是es6的箭頭函數,不用着急,咱們的.babelrc尚未寫配置呢api

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的版本 ...待補充

全文完

相關文章
相關標籤/搜索