首頁 > 軟體

用了babel還需要polyfill嗎原理解析

2023-02-06 06:00:58

引言

前兩天一個同事跟我說了這麼一個面試題,面試官上來就問他:“專案中用了babel還需要polyfill嗎?” 開始他的內心是懵比的,怎麼還有如此不按套路出牌的問題,按照面試的基本原則,答案一定是需要的,不然還怎麼往下問啊。於是他說“要的”。當面試官深挖下去的時候他終於頂不住了。 其實平時開發的過程中用的大部分都是現成的腳手架,很少會仔細看babel的設定,更不會深挖babel的用法。那麼今天我就來給大家好好回答一下這個問題。

首先說明:我們後面說的babel都是基於7.10.0這個版本。

啥是Babel

甩出中文官方檔案的定義

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

  • 語法轉換
  • 通過 Polyfill 方式在目標環境中新增缺失的特性 (通過 @babel/polyfill 模組)
  • 原始碼轉換 (codemods)
  • 更多! (檢視這些 視訊 獲得啟發)

看完官方定義之後大家是不是覺得babel一個人就能把向後相容的事情都做完了(不得不說確實有點誤導),其實根本不是這樣的。

真實的情況是babel只是提供了一個“平臺”,讓更多有能力的plugins入駐我的平臺,是這些plugins提供了將 ECMAScript 2015+ 版本的程式碼轉換為向後相容的 JavaScript 語法的能力。

那麼他是咋做到的呢?這就不得不提大名鼎鼎的AST了。

Babel 工作原理

babel的工作過程分為三個階段:parsing(解析)、transforming(轉化)、printing(生成)

  • parsing階段babel內部的 babylon 負責將es6程式碼進行語法分析和詞法分析後轉換成抽象語法樹
  • transforming階段內部的 babel-traverse 負責對抽象語法樹進行變換操作
  • printing階段內部的 babel-generator 負責生成對應的程式碼

其中第二步的轉化是重中之重,babel的外掛機制也是在這一步發揮作用的,plugins在這裡進行操作,轉化成新的AST,再交給第三步的babel-generator。 所以像我們上面說的如果沒有這些plugins進駐平臺,那麼babel這個“平臺”是不具備任何能力的。就好像這樣:

const babel = code => code;

因此我們可以有信心的說出那個答案“需要”。不僅需要polyfill,還需要大量的plugins。

下面我們通過例子來說明,以及還需要哪些plugins才能將 ECMAScript 2015+ 版本的程式碼完美的轉換為向後相容的 JavaScript 語法。

preset-env, polyfill, plugin-transform-runtime 區別

現在我們通過 npm init -y 來建立一個例子,然後安裝 @babel/cli 和 @babel/core。 通過命令 babel index.js --out-file compiled.js 把 index 檔案用 babel 編譯成compiled.js

// index.js
const fn = () => {
  console.log("wens");
};
const p = new Promise((resolve, reject) => {
  resolve("wens");
});
const list = [1, 2, 3, 4].map(item => item * 2);

不加任何plugins

為了印證上面的說法,我們首先測試不加任何plugins的情況,結果如下

//compiled.js
const fn = () => {
  console.log("wens");
};
const p = new Promise((resolve, reject) => {
  resolve("wens");
});
const list = [1, 2, 3, 4].map(item => item * 2);

編譯好的檔案沒有任何變化,印證了我們上面的說法。接下來我們加入 plugins。

在加入plugins測試之前我們需要知道一些前置知識,babel將ECMAScript 2015+ 版本的程式碼分為了兩種情況處理:

  • 語法層: let、const、class、箭頭函數等,這些需要在構建時進行轉譯,是指在語法層面上的轉譯
  • api方法層:Promise、includes、map等,這些是在全域性或者Object、Array等的原型上新增的方法,它們可以由相應es5的方式重新定義

babel對這兩種情況的轉譯是不一樣的,我們需要給出相應的設定。

加入preset-env

上面的例子種const,箭頭函數屬於語法層面的,而promise和map屬於api方法層面的,現在我們加入 preset-env 看看效果

// babel.config.js
module.exports = {
  presets: ["@babel/env"],
  plugins: []
};

babel 官方定義的 presets 設定項表示的是一堆plugins的集合,省的我們一個個的寫plugins,他直接定義好了類似處理react,typescript等的preset

//compiled.js
"use strict";
var fn = function fn() {
  console.log("wens");
};
var p = new Promise(function (resolve, reject) {
  resolve("wens");
});
var list = [1, 2, 3, 4].map(function (item) {
  return item * 2;
});

果然從語法層面都降級了。那麼api層面要如何處理呢? 下面我們加入 @bable/polyfill

加入polyfill

對於 polyfill 的定義,相信經常逛 mdn 的同學一定不會陌生, 他就是把當前瀏覽器不支援的方法通過用支援的方法重寫來獲得支援。

在專案中安裝 @bable/polyfill ,然後 index.js 檔案中引入

// index.js
import "@bable/polyfill";
const fn = () => {
  console.log("wens");
};
const p = new Promise((resolve, reject) => {
  resolve("wens");
});
const list = [1, 2, 3, 4].map(item => item * 2);

再次編譯我們看看結果

// compiled.js
"use strict";
require("@bable/polyfill");
var fn = function fn() {
  console.log("wens");
};
var p = new Promise(function (resolve, reject) {
  resolve("wens");
});
var list = [1, 2, 3, 4].map(function (item) {
  return item * 2;
});

沒有別的變化,就多了一行require("@bable/polyfill"),其實這裡就把這個庫中的所有的polyfill都引入進來了,就好比我們專案中一股腦引入了全部的lodash方法。這樣就支援了Promise和map方法。

細心的同學一定會發現這樣有點“蠢”啊,lodash都提供了按需載入,你這個一下都引入進來了,可我只需要Promise和map啊。別慌,我們接著往下看。

設定 useBuiltIns

上面我們通過 import "@bable/polyfill" 的方式來實現針對api層面的“抹平”。然而從 babel v7.4.0開始官方就不建議採取這樣的方式了。 因為引入 @bable/polyfill 就相當於在程式碼中引入下面兩個庫

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

這意味著不僅不能按需載入還有全域性空間被汙染的問題。因為他是通過向全域性物件和內建物件的prototype上新增方法來實現的。

因此 babel 決定把這兩個人的工作一併交給上面我們提到的@babel/env,不僅不會全域性汙染還支援按需載入,豈不是妙哉。現在我們再來看看改造後的設定

// index.js
// 去掉了polyfill
const fn = () => {
  console.log("wens");
};
const p = new Promise((resolve, reject) => {
  resolve("wens");
});
const list = [1, 2, 3, 4].map(item => item * 2);
// webpack.config.js
module.exports = {
  presets: [
    [
      "@babel/env",
      {
        useBuiltIns: "usage", // 實現按需載入
        corejs: { 
          version: 3, 
          proposals: true 
        }
      }
    ]
  ],
  plugins: []
};

通過給 @babel/env 設定 useBuiltIns 和 corejs 屬性,我們實現了polyfill方法的按需載入。關於全部設定項,參加官方檔案

// compiled.js
"use strict";
require("core-js/modules/es.array.map");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
var fn = function fn() {
  console.log("wens");
};
var p = new Promise(function (resolve, reject) {
  resolve("wens");
});
var list = [1, 2, 3, 4].map(function (item) {
  return item * 2;
});

編譯後的js檔案只require了需要的方法,完美。那麼我們還有可以優化的空間嗎?有的有的,我們接著往下看。

加入 @babel/plugin-transform-runtime

改造上面的例子

// index.js
class Person {
  constructor(name) {
    this.name = name;
  }
  say() {
    console.log(this.name);
  }
}

只轉換一個 Person 類,我們看看轉換後的檔案長啥樣

// compiled.js 
"use strict";
require("core-js/modules/es.function.name");
require("core-js/modules/es.object.define-property");
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
var Person = /*#__PURE__*/function () {
  function Person(name) {
    _classCallCheck(this, Person);
    this.name = name;
  }
  _createClass(Person, [{
    key: "say",
    value: function say() {
      console.log(this.name);
    }
  }]);
  return Person;
}();

除了require的部分,還多了好多自定義的函數。同學們想一想,現在只有一個index檔案需要轉換,然而實際專案開發中會有大量的需要轉換的檔案,如果每一個轉換後的檔案中都存在相同的函數,那豈不是浪費了,怎麼才能把重複的函數去掉呢?

plugin-transform-runtime 閃亮登場。

上面出現的_classCallCheck,_defineProperties,_createClass三個函數叫做輔助函數,是在編譯階段輔助 Babel 的函數。

當使用了plugin-transform-runtime外掛後,就可以將babel轉譯時新增到檔案中的內聯輔助函數統一隔離到babel-runtime提供的helper模組中,編譯時,直接從helper模組載入,不在每個檔案中重複的定義輔助函數,從而減少包的尺寸,下面我們看下效果:

// webpack.config.js
module.exports = {
  presets: [
    [
      "@babel/env",
      {
        useBuiltIns: "usage",
        corejs: { version: 3, proposals: true }
      }
    ]
  ],
  plugins: ["@babel/plugin-transform-runtime"]
};
// compiled.js
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
require("core-js/modules/es.function.name");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var Person = /*#__PURE__*/function () {
  function Person(name) {
    (0, _classCallCheck2["default"])(this, Person);
    this.name = name;
  }
  (0, _createClass2["default"])(Person, [{
    key: "say",
    value: function say() {
      console.log(this.name);
    }
  }]);
  return Person;
}();

完美的解決了程式碼冗餘的問題。 你們以為這就結束了嗎,還沒有。仔細看到這裡的同學應該注意到了雖然上面使用 useBuiltIns 設定項實現了poilyfill的按需參照,可是他還存在全域性變數汙染的情況,就好比這句程式碼,重寫了array的prototype方法,造成了全域性汙染。

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

最後再改造一次babel的組態檔

// webpack.config.js
module.exports = {
  presets: ["@babel/env"],
  plugins: [
    [
      "@babel/plugin-transform-runtime",
      {
        corejs: { version: 3 }
      }
    ]
  ]
};

我們看到去掉了 @babel/env 的相關引數,而給 plugin-transform-runtime 新增了corejs引數,最終轉換後的檔案不會再出現polyfill的require的方法了。

// compiled.js
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass"));
var Person = /*#__PURE__*/function () {
  function Person(name) {
    (0, _classCallCheck2["default"])(this, Person);
    this.name = name;
  }
  (0, _createClass2["default"])(Person, [{
    key: "say",
    value: function say() {
      console.log(this.name);
    }
  }]);
  return Person;
}();

綜上所述,plugin-transform-runtime 外掛藉助babel-runtime實現了下面兩個重要的功能

  • 對輔助函數的複用,解決轉譯語法層時出現的程式碼冗餘
  • 解決轉譯api層出現的全域性變數汙染

結束

終於寫完了,從翻閱檔案到產出文章足足耗費了兩天的時間。希望看到文章的同學也能和我一樣有收穫~~

更多關於babel polyfill原理的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com