babel能不能分析程式碼然後按需polyfill ?

碎碎醬發表於2018-12-07

原文地址: github.com/yinxin630/b…
技術交流群: fiora.suisuijiang.com/

先聊下 babel 與 polyfill

ES2015 標準已經發布三年了, 在專案中我們會寫 ES2015(或者更高版本) 的程式碼, 但是程式碼最終執行的環境(瀏覽器)通常是不可控的, 因此需要將 ES2015 編譯為低版本程式碼, 來保證所有目標環境可執行

babel 就是用來將高版本編譯為低版本的工具, 在不配置額外外掛的情況下, babel 僅僅是將 ES2015 的語法(例如for of)轉換, 而 ES2015 新增的類/方法(例如Set 或者 [1, 2].findIndex())會保持原樣

這時候就需要 polyfill 了, 需要在專案入口檔案最開頭引入@babel/polyfill. 但是在專案中, 通常僅用到了有限的 polyfill 內容, 而最新版的@babel/polyfill包體積有 81.2k(gzipped 27.7k) 大小

那麼可不可以只 polyfill 程式碼中用到的內容呢?

假設有如下原始碼:

const set = new Set(); // ES6 Set
set.add(1);
set.add(2);
set.add(3);

const arr = [1, 2, 3]; // ES6 for..of
for (const a of arr) {
    console.log(a);
}

console.log(arr.findIndex(x => x === 2));  // ES6 Array.prototype.findIndex
複製程式碼

接下來試試不同的 polyfill 方案

@babel/plugin-transform-runtime

babeljs.io/docs/en/bab…

首先是使用 transfrom-runtime 這個外掛, 它可以僅對程式碼中用到的類/靜態方法進行 polyfill, 但是對於原型鏈上新增的方法無效

NOTE: Instance methods such as "foobar".includes("foo") will not work since that would require modification of existing built-ins (you can use @babel/polyfill for that).

新增 babel 配置

// babel 配置
{
    "presets": [
        [
            "@babel/preset-env",
        ]
    ],
    "plugins": [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": 2,
                "helpers": true,
                "regenerator": true,
                "useESModules": false
            }
        ]
    ]
}
複製程式碼

編譯後:

"use strict";

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

var _set = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/set"));

var set = new _set.default();
set.add(1);
set.add(2);
set.add(3);
var arr = [1, 2, 3];

for (var _i = 0; _i < arr.length; _i++) {
  var a = arr[_i];
  console.log(a);
}

console.log(arr.findIndex(function (x) {
  return x === 2;
}));
複製程式碼

編譯後的程式碼僅僅引入了 Set 實現, 但是 findIndex() 沒有 polyfill 如果你確定不會使用任何原型鏈上新增的方法, 那麼 @babel/plugin-transform-runtime 會是一個不錯的選擇

@babel/preset-env + useBuiltIns

babeljs.io/docs/en/bab…

@babel/preset-env 支援你配置目標環境, 它的 useBuiltIns 選項, 有三個可選值 "usage" | "entry" | false(預設值)

This option adds direct references to the core-js module as bare imports. Thus core-js will be resolved relative to the file itself and needs to be accessible. You may need to specify core-js@2 as a top level dependency in your application if there isn't a core-js dependency or there are multiple versions.

useBuiltIns: 'entry'

該選項需要在專案中引入 @babel/polyfill, babel 會自動將 @babel/polyfill 分解為更小的、僅目標環境需要的 polyfill 引用

NOTE: Only use require("@babel/polyfill"); once in your whole app. Multiple imports or requires of @babel/polyfill will throw an error since it can cause global collisions and other issues that are hard to trace. We recommend creating a single entry file that only contains the require statement.

首先要在原始碼第一行新增 polyfill 引用

import '@babel/polyfill'
複製程式碼

修改 babel 配置

// babel 配置
{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": "Chrome 40",
                "useBuiltIns": "entry"
            }
        ]
    ]
}
複製程式碼

編譯後:

"use strict";

require("core-js/modules/es6.array.copy-within");

require("core-js/modules/es6.array.fill");

require("core-js/modules/es6.array.find");

require("core-js/modules/es6.array.find-index");

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

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

require("core-js/modules/es6.array.of");

require("core-js/modules/es6.array.sort");

require("core-js/modules/es6.array.species");

require("core-js/modules/es6.date.to-primitive");

require("core-js/modules/es6.function.has-instance");

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

require("core-js/modules/es6.number.constructor");

require("core-js/modules/es6.object.assign");

require("core-js/modules/es7.object.define-getter");

require("core-js/modules/es7.object.define-setter");

require("core-js/modules/es7.object.entries");

require("core-js/modules/es6.object.freeze");

require("core-js/modules/es6.object.get-own-property-descriptor");

require("core-js/modules/es7.object.get-own-property-descriptors");

require("core-js/modules/es6.object.get-prototype-of");

require("core-js/modules/es7.object.lookup-getter");

require("core-js/modules/es7.object.lookup-setter");

require("core-js/modules/es6.object.prevent-extensions");

require("core-js/modules/es6.object.is-frozen");

require("core-js/modules/es6.object.is-sealed");

require("core-js/modules/es6.object.is-extensible");

require("core-js/modules/es6.object.seal");

require("core-js/modules/es7.object.values");

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

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

require("core-js/modules/es6.reflect.apply");

require("core-js/modules/es6.reflect.construct");

require("core-js/modules/es6.reflect.define-property");

require("core-js/modules/es6.reflect.delete-property");

require("core-js/modules/es6.reflect.get");

require("core-js/modules/es6.reflect.get-own-property-descriptor");

require("core-js/modules/es6.reflect.get-prototype-of");

require("core-js/modules/es6.reflect.has");

require("core-js/modules/es6.reflect.is-extensible");

require("core-js/modules/es6.reflect.own-keys");

require("core-js/modules/es6.reflect.prevent-extensions");

require("core-js/modules/es6.reflect.set");

require("core-js/modules/es6.reflect.set-prototype-of");

require("core-js/modules/es6.regexp.constructor");

require("core-js/modules/es6.regexp.flags");

require("core-js/modules/es6.regexp.match");

require("core-js/modules/es6.regexp.replace");

require("core-js/modules/es6.regexp.split");

require("core-js/modules/es6.regexp.search");

require("core-js/modules/es6.regexp.to-string");

require("core-js/modules/es6.set");

require("core-js/modules/es6.symbol");

require("core-js/modules/es7.symbol.async-iterator");

require("core-js/modules/es6.string.code-point-at");

require("core-js/modules/es6.string.ends-with");

require("core-js/modules/es6.string.from-code-point");

require("core-js/modules/es6.string.includes");

require("core-js/modules/es7.string.pad-start");

require("core-js/modules/es7.string.pad-end");

require("core-js/modules/es6.string.raw");

require("core-js/modules/es6.string.repeat");

require("core-js/modules/es6.string.starts-with");

require("core-js/modules/es6.typed.array-buffer");

require("core-js/modules/es6.typed.int8-array");

require("core-js/modules/es6.typed.uint8-array");

require("core-js/modules/es6.typed.uint8-clamped-array");

require("core-js/modules/es6.typed.int16-array");

require("core-js/modules/es6.typed.uint16-array");

require("core-js/modules/es6.typed.int32-array");

require("core-js/modules/es6.typed.uint32-array");

require("core-js/modules/es6.typed.float32-array");

require("core-js/modules/es6.typed.float64-array");

require("core-js/modules/es6.weak-map");

require("core-js/modules/es6.weak-set");

require("core-js/modules/web.timers");

require("core-js/modules/web.immediate");

require("core-js/modules/web.dom.iterable");

require("regenerator-runtime/runtime");

var set = new Set();
set.add(1);
set.add(2);
set.add(3);
var arr = [1, 2, 3];

for (var _i = 0; _i < arr.length; _i++) {
  var a = arr[_i];
  console.log(a);
}

console.log(arr.findIndex(function (x) {
  return x === 2;
}));
複製程式碼

編譯後的程式碼自動引入的 Chrome 40 不支援的所有內容, 包括 SetfindIndex(), 它並不會去分析原始碼用到的哪些內容

嘗試修改 targets 為 Chrome 60, 編譯後:

"use strict";

require("core-js/modules/es6.array.sort");

require("core-js/modules/es7.object.define-getter");

require("core-js/modules/es7.object.define-setter");

require("core-js/modules/es7.object.lookup-getter");

require("core-js/modules/es7.object.lookup-setter");

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

require("core-js/modules/es7.symbol.async-iterator");

require("core-js/modules/web.timers");

require("core-js/modules/web.immediate");

require("core-js/modules/web.dom.iterable");

const set = new Set();
set.add(1);
set.add(2);
set.add(3);
const arr = [1, 2, 3];

for (const a of arr) {
  console.log(a);
}

console.log(arr.findIndex(x => x === 2));
複製程式碼

由於 Chrome 60 已經支援 SetfindIndex() 了, 因此 polyfill 的內容並不包括它倆

useBuiltIns: 'usage'

該選專案前還是實驗性的, 我們來試試它打包後是怎樣的

首先要去掉原始碼中的 import '@babel/polyfill'

修改 babel 配置:

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": "Chrome 40",
                "useBuiltIns": "usage"
            }
        ]
    ]
}
複製程式碼

編譯後:

"use strict";

require("core-js/modules/es6.array.find-index");

require("core-js/modules/web.dom.iterable");

require("core-js/modules/es6.set");

var set = new Set();
set.add(1);
set.add(2);
set.add(3);
var arr = [1, 2, 3];

for (var _i = 0; _i < arr.length; _i++) {
  var a = arr[_i];
  console.log(a);
}

console.log(arr.findIndex(function (x) {
  return x === 2;
}));
複製程式碼

哇, 看起來這似乎就是我需要的! 但它是分析到我使用了 Array.protoptype.findIndex() 才新增 polyfill 的嗎? 來做個試驗看看

修改原始碼:

String.prototype.findIndex = function() {}
const str = '';
str.findIndex(); // 呼叫 String.prototype.findIndex
複製程式碼

這次我沒有去呼叫 Array 原型鏈的 findIndex, 而且呼叫了自己實現的 String 原型鏈的 findIndex

編譯後:

"use strict";

require("core-js/modules/es6.array.find-index");

String.prototype.findIndex = function () {};

var str = '';
str.findIndex();
複製程式碼

?原來它是直接匹配的方法名, 新增了同名的 polyfill

useBuiltIns 結論

useBuiltIns: 'entry' 是按目標環境去 polyfill 的, 不關心程式碼中是否使用, 可以保證在目標環境一定可用

useBuiltIns: 'usage' 目前還是實驗性的配置, 它會分析程式碼呼叫, 但是對於原型鏈上的方法僅僅按照方法名去匹配, 可以得到更小的 polyfill 體積. 但是它不會去分析程式碼依賴的 npm 包的內容, 如果某個 npm 包是需要一些 polyfill 的, 那這些 polyfill 並不會被打包進去

為什麼原型鏈上的方法不能根據是否用到, 然後按需去 polyfill 呢?

主要是因為 JavaScript 動態型別的特性, 有些變數/例項的型別是執行時才能確定的, 而 babel 僅僅是對程式碼的靜態編譯, 因此它並不能確定 findIndex() 到底是不是 Array.protoptype.findIndex(), 例如:

fetch('/api')
.then(res => res.json())
.then(data => data.findIndex)
複製程式碼

data 的型別由執行時介面返回的內容決定, 所以 babel 不能實現原型鏈方法按需 polyfill

TypeScript 具備靜態型別, 可以按需 polyfill 嗎?

結論是不能! 關於 polyfill 的討論可以看看 github.com/Microsoft/T…

TypeScript 可以用 --lib 引數指定要依賴的庫, 搭配 ts-polyfill 可以對依賴的庫進行 polyfill, 但是指定依賴時不能詳細到某個方法, 只能 ESNext.Array

如果非要只 polyfill SetfindIndex 呢?

可以手動引入 core-js 中相應的實現, 譬如:

import 'core-js/modules/es6.set.js';
import 'core-js/modules/es6.array.find-index.js';
複製程式碼

不推薦這種做法, 除非追求最小的 polyfill 大小, 你必須清楚的知道專案中用到了哪些內容. 但在實際專案中, 尤其多人開發的專案, 通常很難去控制

相關文章