我眼中的Babel 7

前言

看着Babel的官方文檔學習實在是困難,大概仍是由於那年英語太差,所幸有不少大大們對文檔進行了翻譯。另外看了不少前輩們的文章,受益不淺。可是紙上得來終覺淺,眼睛老是在欺騙我。當我覺得我看懂了,閉上眼睛自省一下:「我會了什麼?」這個時候才發現一切都是我覺得的。因此又從新整理了本身的思路,並寫了下來,方便回顧。node

Babel是什麼?

Babel 是一個 JavaScript 編譯器react

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

Babel 能幹什麼?
web

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

通俗地講,Babel 只是轉移新標準引入的語法,例如 ES6 中的箭頭函數、解構等。而新標準中新增的方法、函數等就須要經過 Polyfill 在目標環境中添加缺失的特性(即新標準中新增的方法、函數等)來解決。typescript

Babel編譯的過程分爲三個階段:npm

  • 解析:將代碼字符串解析成抽象語法樹。
  • 轉換:對抽象語法樹進行轉換操做。
  • 輸出:根據變換後的抽象語法樹再生成代碼字符串。

本文主要分析的是三個階段中的 轉換,大部分的內容參考自 Babel官網json

Babel雖然能夠開箱即用,可是若是什麼都不配置,那麼它會將代碼解析以後再輸出一樣的代碼。因此咱們須要經過配置插件(Plugins)和預設(Preset)來轉換咱們的代碼。promise

準備

爲了更清楚的瞭解 Babel 是如何 「起做用」 的,咱們能夠進行如下的準備工做。瀏覽器

  • 建立demobash

    新建文件夾babel-test,進入該目錄下。使用 npm init -y 初始化,新建src/index.js文件,並添加文件內容const fn = () => 1;

  • 安裝必要的依賴

    @babel/core是 Babel 的核心,包含了全部的核心 API。
    @babel/cli是命令行工具,爲咱們提供 babel 命令來編譯文件。

    # 當前文章安裝的版本:
    # @babel/core: ^7.9.0
    # @babel/cli: ^7.8.4
    npm install --save-dev @babel/core @babel/cli
    複製代碼
  • 添加編譯命令

    package.json文件中的的script字段下添加一項:

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

    該命令行的做用是將babel-test/src目錄下的每一個JavaScript文件轉換並輸出到babel-test/lib目錄。

目前package.json文件內容以下圖所示:

此時執行 npm run trans咱們會發現 lib/index.js中輸出的內容和 src/index.js是一致的,這是由於咱們沒有告訴 Babel應該執行什麼樣的轉換,因此下面就須要咱們設置插件和預設來解決這個問題。

插件

插件用於轉換咱們的代碼,分爲兩種:語法插件和轉換插件。

  • 語法插件

    只容許Babel解析(parse)特定類型的語法(而不是轉換)。

  • 轉換插件

    轉換插件將啓用相應的語法插件,所以咱們沒必要同時指定這兩個插件。

    當咱們啓用轉換插件時,會自動啓用相應的語法插件進行解析,而後經過轉換插件進行轉換。想要詳細瞭解有哪些轉換插件能夠看這裏:插件

  • 插件的使用

    若是插件是在項目根目錄下經過 npm 安裝的,那咱們能夠輸入插件的名稱,babel 會自動檢查它是否已經被安裝到node_modules目錄下。

    要將src/index.js中的箭頭函數轉換成普通函數,咱們能夠藉助官方提供的轉換插件@babel/plugin-transform-arrow-functions

    npm isntall --save-dev @babel/plugin-transform-arrow-functions
    複製代碼

    新建bebal-test/.babelrc文件並添加上面安裝好的插件,文件內容以下:

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

    配置完成後,再次執行npm run trans命令,咱們能夠看到lib/index.js文件中已是咱們想要的內容了。

    固然,在實際開發中,若是咱們一個一個這樣去配置轉換插件,那也麻煩了。所幸Babel爲咱們提供了預設Preset

預設

Preset 能夠做爲 Babel 插件的組合。

官方針對咱們經常使用的環境編寫了一些Preset

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

建立 Preset

如需建立preset,導出一份配置便可。

module.exports = function() {
  return {
    plugins: [
      "pluginA",
      "pluginB",
      "pluginC",
    ]
  };
}
複製代碼

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

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

@babel/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). This both makes your life easier and JavaScript bundles smaller!(官網)

意思是:@babel/preset-env是一個靈活的預設,你不須要管理目標環境須要的語法轉換瀏覽器polyfills,就可使用最新的 JavaScript,同時也會讓 JavaScript 打包後的文件更小。

那麼@babel/preset-env的做用是什麼呢?

  • 將 JavaScript 引入的新語法轉換成ES5的語法。
  • 加載瀏覽器polyfills。

須要注意的是,@babel/preset-env它不支持stage-x插件。

安裝

# @babel/preset-env: ^7.9.0
npm install --save-dev @babel/preset-env
複製代碼

修改.babelrc文件內容:

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

栗子🌰

修改src/index.js中的內容以下所示:

Array.from('foo'); // ['f', 'o', 'o']

Array.from([1, 2, 3], x => x + x); // [2, 4, 6]

let promsie = new Promise();
複製代碼

執行編譯命令npm run translib/index.js中的內容以下所示:

"use strict";

Array.from('foo'); // ['f', 'o', 'o']

Array.from([1, 2, 3], function (x) {
  return x + x;
}); // [2, 4, 6]

let promsie = new Promise();
複製代碼

經過上面兩段代碼的對比,咱們能夠看到只有原先的箭頭函數發生了轉換。而Array.from方法和Promise構造函數並無發生轉換。

小結:

由於@babel/preset-env轉換的是語法,不包含新增的全局變量、方法等,因此須要加載瀏覽器polyfills來完善代碼轉換。(後面介紹完Polyfill會對.babelrc文件進行完善)

Browserslist 集成

@babel/preset-env會拿到咱們指定的目標環境,檢查這些映射表來編譯一系列的插件並傳給 Babel。針對基於瀏覽器的或Electron-based的項目,官方推薦使用.browserslistrc文件來指定目標環境。如果咱們沒有設置targets或ignoreBrowserslistConfig配置項,@babel/preset-env默認會使用.browserslistrc中的配置。

好比,當咱們只想包括大於0.25%市場份額的瀏覽器的那些polyfill和代碼轉換:

  • options(關於@babel/preset-env的更多options配置

    {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": "> 0.25%, not dead"
          }
        ]
      ]
    }
    複製代碼
  • browserslist

    > 0.25%
    not dead
    複製代碼
  • package.json

    {
      "browserslist": "> 0.25%, not dead"
    }
    複製代碼

Polyfill

polyfill 的中文意思是墊片,用來墊平不一樣目標環境下的差別,讓新的內置函數、實例方法等在低版本瀏覽器中也可使用。

Babel 7.4.0 版本開始,@babel/polyfill 已經被廢棄不推薦使用,支持直接導入 core-js/stable(polyfill ECMAScript 特性)和 regenerator-runtime/runtime(須要使用轉換後的 generator 函數)(官網)。

這裏我就根據本身的理解介紹一下 Babel 7.4.0 版本以後 Polyfill 的用法,若是有理解錯的地方,還請各位看官大大幫我指出。

core-jsregenerator-runtime將模擬完整的ES2015 +環境(不包含第4階段的提議),在 JavaScript 代碼執行前引入。

This means you can use new built-ins like Promise or WeakMap, static methods like Array.from or Object.assign, instance methods like Array.prototype.includes, and generator functions (provided you use the regenerator plugin). The polyfill adds to the global scope as well as native prototypes like String in order to do this.

意思是咱們可使用: 新的內置函數 如 Promise 和 WeakMap; 新的靜態方法 如 Array.from 和 Object.assign; 新的實例方法 如 Array.prototype.includes和generator函數(前提是使用了 @babel/plugin-transform-regenerator 插件)。 polyfill將添加到全局範圍以及本機原型(如String)中,以便執行此操做。

安裝core-jsregenerator-runtime

# core-js: ^3.6.4;提供 es 新的特性。
# regenerator: ^0.14.4;應用代碼中用到generator、async函數的話,提供對 generator 支持。
npm install --save core-js regenerator
複製代碼

安裝完成後修改src/index.js文件。

import "core-js/stable";
import "regenerator-runtime/runtime";

Array.from('foo'); // ['f', 'o', 'o']

Array.from([1, 2, 3], x => x + x); // [2, 4, 6]

let promsie = new Promise();
複製代碼

執行npm run trans命令後,查看lib/index.js中的內容。

"use strict";

require("core-js/stable");

require("regenerator-runtime/runtime");

Array.from('foo'); // ['f', 'o', 'o']

Array.from([1, 2, 3], function (x) {
  return x + x;
}); // [2, 4, 6]

var promsie = new Promise();
複製代碼

此時咱們的代碼在低版本瀏覽器中已經可以正常運行了。

經過webpack打包後,發現包的大小爲127kb。這是由於咱們引入了所有的Polyfill致使壓縮後包的體積變大,因此咱們更但願按需引入,所幸Babel已經爲咱們提供瞭解決方案(webpack的配置文件在文末會貼出來)。

@babel/preset-env 提供了一個 useBuiltIns 參數,設置值爲 usage 時,就只會包含代碼須要的 polyfill 。設置該參數時,必需要同時設置 corejs,前面已經安裝了這裏就不重複提了。(core-js@2已經不會再添加新特性,新特性都會添加到 core-js@3)

優化

修改配置文件.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}
複製代碼

修改src/index.js

Array.from('foo'); // ['f', 'o', 'o']

Array.from([1, 2, 3], x => x + x); // [2, 4, 6]

let promsie = new Promise();

async function fn() {
  return 1
}
複製代碼

執行npm run trans命令後,查看lib/index.js中的內容。

"use strict";

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

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

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

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

require("regenerator-runtime/runtime");

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); 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); }); }; }

Array.from('foo'); // ['f', 'o', 'o']

Array.from([1, 2, 3], function (x) {
  return x + x;
}); // [2, 4, 6]

var promsie = new Promise();

function fn() {
  return _fn.apply(this, arguments);
}

function _fn() {
  _fn = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            return _context.abrupt("return", 1);

          case 1:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return _fn.apply(this, arguments);
}
複製代碼

從輸出結果能夠看到,咱們已經實現了按需引入Polyfill,再次打包後發現包的大小已經變成了30kb,效果非常顯著,可是這種使用方式始終存在污染全局環境的問題。

爲了解決這個問題,Babel給咱們提供了@babel-runtime。它將開發者依賴的全局內置對象等,抽取成單獨的模塊,並經過模塊導入的方式引入,避免了對全局環境的污染。

@babel/runtime

@babel/runtime是一個包含 Babel modular runtime helpers 和 regenerator-runtime 的庫。

Polyfill的區別:

  • Polyfill 會修改(覆蓋)新增的內置函數、靜態方法和實例方法。

  • @babel/runtime 不會,它只是引入一些 helper 函數,創造對應的方法。

安裝@babel-runtime是代碼運行時須要的依賴,因此須要做爲生產依賴安裝)

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

有時Babel可能會在輸出中注入一些跨文件的相同代碼,所以可能會被重用。

栗子🌰

修改src/index.js中的內容:

class Parent  {}
複製代碼

執行編譯命令npm run translib/index.js中的內容:

"use strict";

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

var Parent = function Parent() {
  _classCallCheck(this, Parent);
};
複製代碼

這意味着每一個包含類的文件都將引入_classCallCheck,重複的代碼注入必然致使包的變大。這個時候就須要使用插件@babel/plugin-transform-runtime

@babel/plugin-transform-runtime 用於構建過程的代碼轉換,而 @babel/runtime 是提供幫助方法的模塊,這樣就能夠避免重複的代碼注入。

@babel/plugin-transform-runtime

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

Babel使用很小的幫助器來完成例如Class的功能。默認狀況下,它將被添加到須要它的每一個文件中。有時不須要重複,特別是當咱們的應用程序分佈在多個文件中時。

這是@babel/plugin-transform-runtime插件的來源:全部幫助程序都將引用該模塊,@babel/runtime以免在編譯後的輸出中出現重複。運行時將被編譯到咱們的構建中。

該轉換器的另外一個目的是爲咱們的代碼建立一個沙盒環境。若是直接引入core-js@babel/polyfill ,它提供了諸如內置插件Promise,Set和Map等,這些會污染全局環境。儘管這對於應用程序或命令行工具多是能夠的,可是若是咱們的代碼是要發佈供他人使用的庫,或者咱們沒法徹底控制代碼運行的環境,則將成爲一個問題。

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

安裝

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

避免重複注入

修改.babelrc文件內容:

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

執行編譯命令npm run trans

"use strict";

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

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

var Parent = function Parent() {
  (0, _classCallCheck2["default"])(this, Parent);
};
複製代碼

lib/index.js中輸出的內容能夠看出classCallCheck不是直接注入到代碼中,而是從 @babel/runtime 中引入,這就避免了相同代碼的重複注入。

避免全局污染

經過添加配置避免全局環境被污染。

安裝@babel/runtime-corejs3

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

修改src/index.js

Array.from('foo'); // ['f', 'o', 'o']

Array.from([1, 2, 3], x => x + x); // [2, 4, 6]

let promsie = new Promise();

async function fn() {
  return 1
}
複製代碼

修改.babelrc

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

複製代碼

執行編譯命令npm run translib/index.js中的內容:

"use strict";

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

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

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

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

var _from = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/array/from"));

(0, _from["default"])('foo'); // ['f', 'o', 'o']

(0, _from["default"])([1, 2, 3], function (x) {
  return x + x;
}); // [2, 4, 6]

var promsie = new _promise["default"]();

function fn() {
  return _fn.apply(this, arguments);
}

function _fn() {
  _fn = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
    return _regenerator["default"].wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            return _context.abrupt("return", 1);

          case 1:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return _fn.apply(this, arguments);
}
複製代碼

從輸出文件中能夠看到,@babel/plugin-transform-runtime經過模塊導入的方式引入所需的功能代碼,避免了對全局環境的污染。

補充

Plugin和Preset的執行順序

  • Plugin 在 Presets 前運行。
  • Plugin 的執行順序是從前日後。
  • Preset 的執行順序是從後往前。

webpack配置文件

  • 安裝必要的依賴
# 本文使用的webpack相關的版本
# webpack-cli@3.3.11
# webpack@4.42.0
# clean-webpack-plugin@3.0.0
# babel-loader@8.1.0
npm install --save-dev webpack-cli webpack babel-loader clean-webpack-plugin
複製代碼
  • 新建babel-test/webpack.config.js文件
const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  mode: 'production',
  entry: './lib/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[hash].js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader'
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin()
  ]
}

複製代碼
  • 添加命令

    package.json文件中的的script字段下添加一項:

    {
      ...
      "scripts": {
        "build": "webpack --mode=production"
      }
      ...
    }
    複製代碼
  • 執行命令

    npm run build
    複製代碼

總結

紙上得來終覺淺,絕知此事要躬行。

另外也謝謝各位大大的文章,讓我受益不淺,下面的連接能夠進去看看哦。 7

若是這篇文章一樣對你有幫助,那就給我留個贊吧,謝謝~



參考連接
* Babel官網
* 不可錯過的 Babel7 知識
* Babel 社區概覽* ...

相關文章
相關標籤/搜索