不容錯過的 Babel7 知識

Babel 的配置項的做用不那麼瞭解,是否會影響平常開發呢?老實說,大多狀況下沒有特別大的影響(畢竟有搜索引擎)。javascript

不過呢,仍是想更進一步瞭解下,因而最近認真閱讀了 Babel 的文檔,外加不斷編譯驗證,輸出了本篇文章,爲了更好的閱讀體驗,修修改改,最終算是以我我的比較喜歡的方式推動了每一個知識點(每個配置的引入都是有緣由的),但願可以幫助你對 Babel 的各類配置有一個更清晰的認識 (已經很懂的小夥伴,無視本文) 。html

Babel 是一個 JS 編譯器java

Babel 是一個工具鏈,主要用於將 ECMAScript 2015+ 版本的代碼轉換爲向後兼容的 JavaScript 語法,以便可以運行在當前和舊版本的瀏覽器或其餘環境中。node

咱們先看看 Babel 可以作什麼:react

  • 語法轉換
  • 經過 Polyfill 方式在目標環境中添加缺失的特性(@babel/polyfill模塊)
  • 源碼轉換(codemods)

本篇文章的目的是搞明白 Babel 的使用和配置,搞清楚 @babel/runtime@babel/polyfill@babel/plugin-transform-runtime 這些做用是什麼,插件和預設都是用來幹什麼的,咱們爲何須要配置它們,而不是講如何進行 AST 轉換,若是你對 AST 轉換很是感興趣,歡迎閱讀咱們的 RN轉小程序引擎 Alita 的源碼,其中應用了大量的 AST 轉換。webpack

更多文章可戳(如Star,謝謝你):https://github.com/YvetteLau/Bloggit

爲了更清晰的瞭解每一步,首先建立一個新項目,例如 babelTemp(你愛取啥名取啥名),使用 npm init -y 進行初始化,建立 src/index.js,文件內容以下(你也能夠隨便寫點什麼):github

const fn = () => {
    console.log('a');
};
複製代碼

OK,建立好的項目先放在一邊,先了解下理論知識:web

核心庫 @babel/core

Babel 的核心功能包含在 @babel/core 模塊中。看到 core 這個詞了吧,意味着核心,沒有它,在 babel 的世界裏註定步履維艱。不安裝 @babel/core,沒法使用 babel 進行編譯。chrome

CLI命令行工具 @babel/cli

babel 提供的命令行工具,主要是提供 babel 這個命令,適合安裝在項目裏。

@babel/node 提供了 babel-node 命令,可是 @babel/node 更適合全局安裝,不適合安裝在項目裏。

npm install --save-dev @babel/core @babel/cli
複製代碼

如今你就能夠在項目中使用 babel 進行編譯啦(若是不安裝 @babel/core,會報錯噢)

將命令配置在 package.json 文件的 scripts 字段中:

//...
"scripts": {
    "compiler""babel src --out-dir lib --watch"
}
複製代碼

使用 npm run compiler 來執行編譯,如今咱們沒有配置任何插件,編譯先後的代碼是徹底同樣的。

由於 Babel 雖然開箱即用,可是什麼動做也不作,若是想要 Babel 作一些實際的工做,就須要爲其添加插件(plugin)。

插件

Babel 構建在插件之上,使用現有的或者本身編寫的插件能夠組成一個轉換通道,Babel 的插件分爲兩種: 語法插件和轉換插件。

語法插件

這些插件只容許 Babel 解析(parse) 特定類型的語法(不是轉換),能夠在 AST 轉換時使用,以支持解析新語法,例如:

import * as babel from "@babel/core";
const code = babel.transformFromAstSync(ast, {
    //支持可選鏈
    plugins: ["@babel/plugin-proposal-optional-chaining"],
    babelrcfalse
}).code;
複製代碼

轉換插件

轉換插件會啓用相應的語法插件(所以不須要同時指定這兩種插件),這點很容易理解,若是不啓用相應的語法插件,意味着沒法解析,連解析都不能解析,又何談轉換呢?

插件的使用

若是插件發佈在 npm 上,能夠直接填寫插件的名稱, Babel 會自動檢查它是否已經被安裝在 node_modules 目錄下,在項目目錄下新建 .babelrc 文件 (下文會具體介紹配置文件),配置以下:

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

也能夠指定插件的相對/絕對路徑

{
    "plugins": ["./node_modules/@babel/plugin-transform-arrow-functions"]
}
複製代碼

執行 npm run compiler,能夠看到箭頭函數已經被編譯OK, lib/index.js 內容以下:

const fn = function ({
    console.log('a');
};
複製代碼

如今,咱們僅支持轉換箭頭函數,若是想將其它的新的JS特性轉換成低版本,須要使用其它對應的 plugin 。若是咱們一個個配置的話,會很是繁瑣,由於你可能須要配置幾十個插件,這顯然很是不便,那麼有沒有什麼辦法能夠簡化這個配置呢?

有!預設!(感謝強大的 Babel)

預設

經過使用或建立一個 preset 便可輕鬆使用一組插件。

官方 Preset

  • @babel/preset-env
  • @babel/preset-flow
  • @babel/preset-react
  • @babel/preset-typescript

注: 從 Babel v7 開始,因此針對標準提案階段的功能所編寫的預設(stage preset)都已被棄用,官方已經移除了 @babel/preset-stage-x

@babel/preset-env

@babel/preset-env 主要做用是對咱們所使用的而且目標瀏覽器中缺失的功能進行代碼轉換和加載 polyfill,在不進行任何配置的狀況下,@babel/preset-env 所包含的插件將支持全部最新的JS特性(ES2015,ES2016等,不包含 stage 階段),將其轉換成ES5代碼。例如,若是你的代碼中使用了可選鏈(目前,仍在 stage 階段),那麼只配置 @babel/preset-env,轉換時會拋出錯誤,須要另外安裝相應的插件。

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

須要說明的是,@babel/preset-env 會根據你配置的目標環境,生成插件列表來編譯。對於基於瀏覽器或 Electron 的項目,官方推薦使用 .browserslistrc 文件來指定目標環境。默認狀況下,若是你沒有在 Babel 配置文件中(如 .babelrc)設置 targetsignoreBrowserslistConfig@babel/preset-env 會使用 browserslist 配置源。

若是你不是要兼容全部的瀏覽器和環境,推薦你指定目標環境,這樣你的編譯代碼可以保持最小。

例如,僅包括瀏覽器市場份額超過0.25%的用戶所需的 polyfill 和代碼轉換(忽略沒有安全更新的瀏覽器,如 IE10 和 BlackBerry):

//.browserslistrc
0.25%
not dead
複製代碼

查看 browserslist 的更多配置

例如,你將 .browserslistrc 的內容配置爲:

last 2 Chrome versions
複製代碼

而後再執行 npm run compiler,你會發現箭頭函數不會被編譯成ES5,由於 chrome 的最新2個版本都可以支持箭頭函數。如今,咱們將 .browserslistrc 仍然換成以前的配置。

就我們目前的代碼來講,當前的配置彷佛已是OK的了。

咱們修改下 src/index.js

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

const p = new Promise((resolve, reject) => {
    resolve(100);
});
複製代碼

編譯出來的結果爲:

"use strict";

var isHas = [123].includes(2);
var p = new Promise(function (resolve, reject{
  resolve(100);
});
複製代碼

這個編譯出來的代碼在低版本瀏覽器中使用的話,顯然是有問題的,由於低版本瀏覽器中數組實例上沒有 includes 方法,也沒有 Promise 構造函數。

這是爲何呢?由於語法轉換隻是將高版本的語法轉換成低版本的,可是新的內置函數、實例方法沒法轉換。這時,就須要使用 polyfill 上場了,顧名思義,polyfill的中文意思是墊片,所謂墊片就是墊平不一樣瀏覽器或者不一樣環境下的差別,讓新的內置函數、實例方法等在低版本瀏覽器中也可使用。

Polyfill

@babel/polyfill 模塊包括 core-js 和一個自定義的 regenerator runtime 模塊,能夠模擬完整的 ES2015+ 環境(不包含第4階段前的提議)。

這意味着可使用諸如 PromiseWeakMap 之類的新的內置組件、 Array.fromObject.assign 之類的靜態方法、Array.prototype.includes 之類的實例方法以及生成器函數(前提是使用了 @babel/plugin-transform-regenerator 插件)。爲了添加這些功能,polyfill 將添加到全局範圍和相似 String 這樣的內置原型中(會對全局環境形成污染,後面咱們會將不污染全局環境的方法)。

首先,安裝 @babel/polyfill 依賴:

npm install --save @babel/polyfill
複製代碼

注意:不使用 --save-dev,由於這是一個須要在源碼以前運行的墊片。

咱們須要將完整的 polyfill 在代碼以前加載,修改咱們的 src/index.js:

import '@babel/polyfill';

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

const p = new Promise((resolve, reject) => {
    resolve(100);
});
複製代碼

@babel/polyfill 須要在其它代碼以前引入,咱們也能夠在 webpack 中進行配置。

例如:

entry: [
    require.resolve('./polyfills'),
    path.resolve('./index')
]
複製代碼

polyfills.js 文件內容以下:

//固然,還可能有一些其它的 polyfill,例如 stage 4以前的一些 polyfill
import '@babel/polyfill';
複製代碼

如今,咱們的代碼無論在低版本仍是高版本瀏覽器(或node環境)中都能正常運行了。不過,不少時候,咱們未必須要完整的 @babel/polyfill,這會致使咱們最終構建出的包的體積增大,@babel/polyfill的包大小爲89K (當前 @babel/polyfill 版本爲 7.7.0)。

咱們更指望的是,若是我使用了某個新特性,再引入對應的 polyfill,避免引入無用的代碼。

值得慶幸的是, Babel 已經考慮到了這一點。

@babel/preset-env 提供了一個 useBuiltIns 參數,設置值爲 usage 時,就只會包含代碼須要的 polyfill 。有一點須要注意:配置此參數的值爲 usage ,必需要同時設置 corejs (若是不設置,會給出警告,默認使用的是"corejs": 2) ,注意: 這裏仍然須要安裝 @babel/polyfill(當前 @babel/polyfill 版本默認會安裝 "corejs": 2):

首先說一下使用 core-js@3 的緣由,core-js@2 分支中已經不會再添加新特性,新特性都會添加到 core-js@3。例如你使用了 Array.prototype.flat(),若是你使用的是 core-js@2,那麼其不包含此新特性。爲了可使用更多的新特性,建議你們使用 core-js@3

安裝依賴依賴:

npm install --save core-js@3
複製代碼

core-js (點擊瞭解更多) : JavaScript 的模塊化標準庫,包含 PromiseSymbolIterator和許多其餘的特性,它可讓你僅加載必需的功能。

如今,修改 Babel 的配置文件以下:

//.babelrc
const presets = [
    [
        "@babel/env",
        {   
            "useBuiltIns""usage",
            "corejs"3
        }
    ]
]
複製代碼

Babel 會檢查全部代碼,以便查找在目標環境中缺失的功能,而後僅僅把須要的 polyfill 包含進來。

例如,src/index.js 代碼不變:

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

const p = new Promise((resolve, reject) => {
    resolve(100);
});
複製代碼

咱們看看編譯出來的文件(lib/index):

"use strict";

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

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

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

var isHas = [123].includes(2);
var p = new Promise(function (resolve, reject{
    resolve(100);
});
複製代碼

一樣的代碼,咱們用 webpack 構建一下(production 模式),能看到最終的代碼大小僅爲: 20KB。而若是咱們引入整個 @babel/polyfill 的話,構建出的包大小爲:89KB

前面曾提到,在 useBuiltIns 參數值爲 usage 時,仍然須要安裝 @babel/polyfill,雖然咱們上面的代碼轉換中看起來並無使用到,可是,若是咱們源碼中使用到了 async/await,那麼編譯出來的代碼須要 require("regenerator-runtime/runtime"),在 @babel/polyfill 的依賴中,固然啦,你也能夠只安裝 regenerator-runtime/runtime 取代安裝 @babel/polyfill

到了這一步,已經很棒棒了,是否是想跳起來轉個圈圈?

下面我要說的內容,也許你已經知道,也許你還不知道,這都不重要,可是此刻起,你要知道了: Babel 會使用很小的輔助函數來實現相似 _createClass 等公共方法。默認狀況下,它將被添加(inject)到須要它的每一個文件中。

假如,咱們的 src/index.js 是這樣的:

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    };
    getX() {
        return this.x;
    }
}

let cp = new ColorPoint(258);
複製代碼

編譯出來的 lib/index.js,以下所示:

"use strict";

require("core-js/modules/es.object.define-property");

function _classCallCheck(instance, Constructorif (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, propsfor (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = trueif ("value" in descriptor) descriptor.writable = trueObject.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticPropsif (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

var Point =
    /*#__PURE__*/
    function ({
        function Point(x, y{
            _classCallCheck(this, Point);

            this.x = x;
            this.y = y;
        }

        _createClass(Point, [{
            key"getX",
            valuefunction getX({
                return this.x;
            }
        }]);

        return Point;
    }();

var cp = new ColorPoint(258);
複製代碼

看起來,彷佛並無什麼問題,可是你想一下,若是你有10個文件中都使用了這個 class,是否是意味着 _classCallCheck_defineProperties_createClass 這些方法被 inject 了10次。這顯然會致使包體積增大,最關鍵的是,咱們並不須要它 inject 屢次。

這個時候,就是 @babel/plugin-transform-runtime 插件大顯身手的時候了,使用 @babel/plugin-transform-runtime 插件,全部幫助程序都將引用模塊 @babel/runtime,這樣就能夠避免編譯後的代碼中出現重複的幫助程序,有效減小包體積。

@babel/plugin-transform-runtime

@babel/plugin-transform-runtime 是一個能夠重複使用 Babel 注入的幫助程序,以節省代碼大小的插件。

注意:諸如 Array.prototype.flat() 等實例方法將不起做用,由於這須要修改現有的內置函數(可使用 @babel/polyfill 來解決這個問題) ——> 對此須要說明的是若是你配置的是corejs3core-js@3 如今已經支持原型方法,同時不污染原型

另外,@babel/plugin-transform-runtime 須要和 @babel/runtime 配合使用。

首先安裝依賴,@babel/plugin-transform-runtime 一般僅在開發時使用,可是運行時最終代碼須要依賴 @babel/runtime,因此 @babel/runtime 必需要做爲生產依賴被安裝,以下 :

npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
複製代碼

除了前文所說的,@babel/plugin-transform-runtime 能夠減小編譯後代碼的體積外,咱們使用它還有一個好處,它能夠爲代碼建立一個沙盒環境,若是使用 @babel/polyfill 及其提供的內置程序(例如 PromiseSetMap ),則它們將污染全局範圍。雖然這對於應用程序或命令行工具多是能夠的,可是若是你的代碼是要發佈供他人使用的庫,或者沒法徹底控制代碼運行的環境,則將成爲一個問題。

@babel/plugin-transform-runtime 會將這些內置別名做爲 core-js 的別名,所以您能夠無縫使用它們,而無需 polyfill

修改 .babelrc 的配置,以下:

//.babelrc
{
    "presets": [
        [
            "@babel/preset-env",
            {
                "useBuiltIns""usage",
                "corejs"3
            }
        ]
    ],
    "plugins": [
        [
            "@babel/plugin-transform-runtime"
        ]
    ]
}
複製代碼

從新編譯 npm run compiler , 如今,編譯出來的內容爲(lib/index.js):

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var Point =
    /*#__PURE__*/
    function ({
        function Point(x, y{
            (0, _classCallCheck2.default)(this, Point);
            this.x = x;
            this.y = y;
        }

        (0, _createClass2.default)(Point, [{
            key"getX",
            valuefunction getX({
                return this.x;
            }
        }]);
        return Point;
    }();

var cp = new ColorPoint(258);
複製代碼

能夠看出,幫助函數如今不是直接被 inject 到代碼中,而是從 @babel/runtime 中引入。前文說了使用 @babel/plugin-transform-runtime 能夠避免全局污染,咱們來看看是如何避免污染的。

修改 src/index.js 以下:

let isHas = [1,2,3].includes(2);

new Promise((resolve, reject) => {
    resolve(100);
});
複製代碼

編譯出來的代碼以下(lib/index.js):

"use strict";

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

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

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

var isHas = [123].includes(2);
new Promise(function (resolve, reject{
    resolve(100);
});
複製代碼

Array.prototype 上新增了 includes 方法,而且新增了全局的 Promise 方法,污染了全局環境,這跟不使用 @babel/plugin-transform-runtime 沒有區別嘛。

若是咱們但願 @babel/plugin-transform-runtime 不只僅處理幫助函數,同時也能加載 polyfill 的話,咱們須要給 @babel/plugin-transform-runtime 增長配置信息。

首先新增依賴 @babel/runtime-corejs3:

npm install @babel/runtime-corejs3 --save
複製代碼

修改配置文件以下(移除了 @babel/preset-envuseBuiltIns 的配置,否則不就重複了嘛嘛嘛,不信的話,你用 async/await 編譯下試試咯):

{
    "presets": [
        [
            "@babel/preset-env"
        ]
    ],
    "plugins": [
        [
            "@babel/plugin-transform-runtime",{
                "corejs"3
            }
        ]
    ]
}
複製代碼

而後從新編譯,看一下,編譯出來的結果(lib/index.js):

"use strict";

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

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

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

var _context;

var isHas = (0, _includes.default)(_context = [123]).call(_context, 2);
new _promise.default(function (resolve, reject{
  resolve(100);
});
複製代碼

能夠看出,沒有直接去修改 Array.prototype,或者是新增 Promise 方法,避免了全局污染。若是上面 @babel/plugin-transform-runtime 配置的 core-js 是 "2",其中不包含實例的 polyfill 須要單獨引入。

劃重點:若是咱們配置的 corejs3 版本,那麼無論是實例方法仍是全局方法,都不會再污染全局環境。

看到這裏,不知道你們有沒有這樣一個疑問?給 @babel/plugin-transform-runtime 配置 corejs 是如此的完美,既能夠將幫助函數變成引用的形式,又能夠動態引入 polyfill,而且不會污染全局環境。何須要給 @babel/preset-env 提供 useBuiltIns 功能呢,看起來彷佛不須要呀。

帶着這樣的疑問,我新建了幾個文件(內容簡單且基本一致,使用了些新特性),而後使用 webpack 構建,如下是我對比的數據:

序號 .babelrc 配置 webpack mode production
0 不使用 @babel/plugin-transform-runtime 36KB
1 使用@babel/plugin-transform-runtime,並配置參數 corejs: 3。不會污染全局環境 37KB
2 使用@babel/plugin-transform-runtime,不配置 corejs 22KB

我猜想是 @babel/runtime-corejs3/XXX 的包自己比 core-js/modules/XXX 要大一些~

插件/預設補充知識

插件的排列順序很重要!!!

若是兩個轉換插件都將處理「程序(Program)」的某個代碼片斷,則將根據轉換插件或 preset 的排列順序依次執行。

  • 插件在 Presets 前運行。
  • 插件順序從前日後排列。
  • Preset 順序是顛倒的(從後往前)。

例如:

{
    "plugins": ["@babel/plugin-proposal-class-properties""@babel/plugin-syntax-dynamic-import"]
}
複製代碼

先執行 @babel/plugin-proposal-class-properties,後執行 @babel/plugin-syntax-dynamic-import

{
  "presets": ["@babel/preset-env""@babel/preset-react"]
}
複製代碼

preset 的執行順序是顛倒的,先執行 @babel/preset-react, 後執行 @babel/preset-env

插件參數

插件和 preset 均可以接受參數,參數由插件名和參數對象組成一個數組。preset 設置參數也是這種格式。

如:

{
    "plugins": [
        [
            "@babel/plugin-proposal-class-properties"
            { "loose"true }
        ]
    ]
}
複製代碼
插件的短名稱

若是插件名稱爲 @babel/plugin-XXX,可使用短名稱@babel/XXX :

{
    "plugins": [
        "@babel/transform-arrow-functions" //同 "@babel/plugin-transform-arrow-functions"
    ]
}
複製代碼

若是插件名稱爲 babel-plugin-XXX,可使用端名稱 XXX,該規則一樣適用於帶有 scope 的插件:

{
    "plugins": [
        "newPlugin"//同 "babel-plugin-newPlugin"
        "@scp/myPlugin" //同 "@scp/babel-plugin-myPlugin"
    ]
}
複製代碼

建立 Preset

能夠簡單的返回一個插件數組

module.exports = function({
    return {
        plugins: [
            "A",
            "B",
            "C"
        ]
    }
}
複製代碼

preset 中也能夠包含其餘的 preset,以及帶有參數的插件。

module.exports = function({
    return {
        presets: [
            require("@babel/preset-env")
        ],
        plugins: [
            [require("@babel/plugin-proposal-class-properties"), { loosetrue }],
            require("@babel/plugin-proposal-object-rest-spread")
        ]
    }
}
複製代碼

配置文件

Babel 支持多種格式的配置文件。這部份內容補充瞭解下便可,誰管你用哪一種配置文件,只要你的配置是OK的就能夠了(敷衍)~

全部的 Babel API 參數均可以被配置,可是若是該參數須要使用的 JS 代碼,那麼可能須要使用 JS 代碼版的配置文件。

根據使用場景能夠選擇不一樣的配置文件:

若是但願以編程的方式建立配置文件或者但願編譯 node_modules 目錄下的模塊:那麼 babel.config.js 能夠知足你的需求。

若是隻是須要一個簡單的而且中用於單個軟件包的配置:那麼 .babelrc 便可知足你的需求。

babel.config.js

在項目根目錄下建立一個名爲 babel.config.js 的文件。

module.exports = function(api{
    api.cache(true);

    const presets = [...];
    const plugins = [...];

    return {
        presets,
        plugins
    };

複製代碼

具體的配置能夠查看:babel.config.js 文檔

.babelrc

在項目根目錄下建立一個名爲 .babelrc 的文件:

{
    "presets": [],
    "plugins": []
}
複製代碼

具體的配置能夠參考 .babelrc 文檔

package.json

能夠將 .babelrc 中的配置信息做爲 babel 鍵(key) 添加到 package.json 文件中:

{
    "name""my-package",
    "babel": {
        "presets": [],
        "plugins": []
    }
}
複製代碼

.babelrc.js

.babelrc 配置相同,可是可使用JS編寫。

//能夠在其中調用 Node.js 的API
const presets = [];
const plugins = [];

module.exports = { presets, plugins };
複製代碼

不知道是否全面,不過真的寫不動了(若有不全,後續再補充)~就醬~若是有錯誤,歡迎指正。

參考連接

  1. babel文檔

  2. babel 7 的使用的我的理解

  3. 一口(很長的)氣了解 babel

  4. core-js@3帶來的驚喜


關注公衆號,加入技術交流羣

相關文章
相關標籤/搜索