@babel/preset-env使用polyfill遇到的坑

wonyun發表於2021-04-16

場景還原

最近將一個專案由babel@6升級到babel@7,升級後最重要的兩個包:

  • @babel/preset-env: 提供程式碼的轉換和API的polyfill的能力
  • @babel/plugin-transform-runtime: 複用babel注入的helper程式碼以及提供無汙染全域性環境的polyfill功能

基於此,對專案中js語法的transform和API的polyfill進行了調整:

  • 關閉@babel/plugin-transform-runtime的polyfill功能
  • 開啟@babel/preset-env的polyfill和transform功能

其中,@babel/preset-env的polyfill使用usage形式(不瞭解的可以檢視官方文件),意思是以專案設定的target環境為前提,根據專案中使用到的api功能進行polyfill;具體babel配置片段如下:

{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": false,
        "regenerator": false
      }
    ]
  ],
  "sourceType": "unambiguous",
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false,
        "shippedProposals": true,
        "useBuiltIns": "usage",
        "corejs": {
          "version": "3.10",
          "proposals": true
        },
        "targets": {
            ...
        }
      }
    ]
  ]
}

然後專案中使用到了Promise.allSettled靜態方法:

Promise.allSettled([p1, p2, p3]).then(res => console.log(res));

通過webpack打包後執行,js會報錯:

TypeError: Promise.allSettled is not a function

不對呀,按照官網就是這麼配置的,一度對babel的配置產生懷疑,折騰半天最後都排除掉;沒招了,那就試試斷點除錯,別說還真發現問題,直接上圖:

相信大家能夠看出問題所在,Promise.allSettled的polyfill之後重新引入Promise的polyfill,後面的Promise的polyfill覆蓋了Promise.allSettled的polyfill,導致呼叫該方法時報錯。

那會不會是babel的bug導致的呢,於是開起查詢問題之旅了。。。

問題追蹤

首先,簡要說明下@babel/preset-env實現polyfill的思路:babel會生成程式碼的ast,並對其traverse過程中,根據程式碼使用的新API來確定需要填充的polyfill。

遇到這種問題,首先想到會不會是@babel/preset-env的bug,google半天也沒有找到類似問題,於是就開啟debug除錯模式。在除錯追蹤到babel-plugin-polyfill-corejs3/lib/index.js中的usageGlobal方法,其在解析程式碼中使用到了PromiseallSettled的api,如下圖:

babel會根據程式碼用到的api,最終解析出為這些api注入的polyfill,如下圖:

從圖可以看出最終需要為PromiseallSettled注入的依賴polyfill;但是注入的polyfill存在問題,即es.promisees.promise.all-settled順序反了,後者依賴前者;由此可見是babel的bug已確定無疑了。

接著進如resolve方法,發現其在確定程式碼的相關polyfill依賴後,對與依賴的先後順序存在bug;因為程式碼呼叫Promise.allSettled會依賴:

  • 全域性global的Promise api
  • Promise的靜態方法allSettled api

所以babel在獲取二者對應的polyfill在合併時產生了問題,這可以在babel-plugin-polyfill-corejs/lib/built-in-definitions.js檔案中:

// 所有靜態方法的polyfill
const StaticProperties = {
    ...
    Promise: {
        all: define(null, PromiseDependenciesWithIterators),
        allSettled: define(null, ["es.promise.all-settled", ...PromiseDependenciesWithIterators]),
        any: define(null, ["esnext.promise.any", ...PromiseDependenciesWithIterators]),
        race: define(null, PromiseDependenciesWithIterators),
        try: define(null, ["esnext.promise.try", ...PromiseDependenciesWithIterators])
      },
  ...
}

可以看出Promise的相關靜態方法的polyfill都放置到第一位,而define為對該數值進行任何排序:

const define = (pure, global, name = global[0], exclude) => {
  return {
    name,
    pure,
    global,
    exclude
  };
};

查到這裡可以猜測這個babel-plugin-polyfill-corejs3@0.1.7有bug,檢視最新版本0.2.0的程式碼發現對這個方法進行了修復:

var _data = _interopRequireDefault(require("../core-js-compat/data.js"));

const polyfillsOrder = {};
Object.keys(_data.default).forEach((name, index) => {
  polyfillsOrder[name] = index;
});

const define = (pure, global, name = global[0], exclude) => {
  return {
    name,
    pure,
    global: global.sort((a, b) => polyfillsOrder[a] - polyfillsOrder[b]),
    exclude
  };
};

可以看出該方法對注入的polyfill做了排序,進過排序得到正確的依賴順序,於是果斷升級@babel/preset-env@7.13.15,因為之前@babel/preset-env@7.13.10依賴的是babel-plugin-polyfill-corejs3@0.1.7,至此一直困擾我的這個大坑給堵上了。

出於好奇心,對babel-plugin-polyfill-corejs3程式碼進行blame,果然發現這個問題在24天前進行了修復:

blame.png

進一步檢視發現,之前已經有人提出過類似的bug:The order of promise and promise.finally after compilation seems to be wrong,於是做了修復。

總結

困擾我一天的問題算是解決了,分享給大家希望大家避坑。

不過話說回來,開始遇到這個問題時,換成@babel/preset-enventry模式的polyfill模式不會發生任何問題,但是心中過不去這個坎為啥usage模式不能用,明明後者有一定的體積優勢,最終得到答案;這一過程雖然耗費一定的時間,但是有收穫,值!

相關文章