場景還原
最近將一個專案由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
方法,其在解析程式碼中使用到了Promise
和allSettled
的api,如下圖:
babel會根據程式碼用到的api,最終解析出為這些api注入的polyfill,如下圖:
從圖可以看出最終需要為Promise
和allSettled
注入的依賴polyfill;但是注入的polyfill存在問題,即es.promise
與es.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天前進行了修復:
進一步檢視發現,之前已經有人提出過類似的bug:The order of promise and promise.finally after compilation seems to be wrong,於是做了修復。
總結
困擾我一天的問題算是解決了,分享給大家希望大家避坑。
不過話說回來,開始遇到這個問題時,換成@babel/preset-env
的entry
模式的polyfill模式不會發生任何問題,但是心中過不去這個坎為啥usage
模式不能用,明明後者有一定的體積優勢,最終得到答案;這一過程雖然耗費一定的時間,但是有收穫,值!