沒想到會是在雙十一這麼忙的時間段把這篇文章寫完 ?,公司很忙很緊張,可我還有時間在公司做分享,寫博文,慚愧慚愧... 做後臺系統在雙十一期間不如 2c 端的小夥伴有參與感呀。
問題根源
上文 介面異常狀態統一處理方案:優先業務端處理,再按需統一處理。 最後提出可能存在的問題:
如果專案是由 vue-cli 搭建的 webpack 模板專案,在沒有修改 .babelrc 檔案配置的情況下,此方案在 Firefox 瀏覽器下是無效的。介面狀態異常的情況下,總是會執行統一處理,不會先交由業務端處理異常,再判定是否執行統一處理。
通過 debug 發現,handleAPIStatusError
函式總是在 catch
函式之前先執行,導致每次都能在 handleAPIStatusError
函式內找到未處理的異常介面的 apiUid
。這就奇怪了,Promise
是 micro-task,setTimeout
是 macro-task,總不可能是 EvenLoop 的問題吧 ?,不可能不可能,想多了。?
那到底是什麼原因呢?只能走程式碼了,Go Go Go... 然後在 Firefox 開發者工具內,發現在 polyfill.js
內 Promise
的墊片函式 then
和 catch
內部,this
的屬性中居然沒有 apiUid
!而且還有一堆莫名其妙不明所以的屬性。?
怎麼肥四??
我表示看不懂,this
不應該是 Promise
的例項嗎?現在這個是個什麼鬼??
經過一番掙扎,突然發現:
“咦,Promise
哪去了?”
“那個 __WEBPACK_IMPORTED_MODULE_0_babel_runtime_core_js_promise___default
是什麼鬼?”
然後... 突然醒悟 ?
babel_runtime_core_js_promise
?
core_js
? babel-polyfill
?
babel_runtime
? babel-transform-runtime
?
難道 Firefox 內建的 Promise
是假的?
一臉驚悚... ?
帶著疑惑的心態去查詢 babel-polyfill
的原始碼,找到 Promise
的墊片函式:
*** node_modules/core-js/modules/es6.promise.js ***
// ...
var USE_NATIVE = !!(function() {
try {
// correct subclassing with @@species support
var promise = $Promise.resolve(1);
var FakePromise = ((promise.constructor = {})[
require("./_wks")("species")
] = function(exec) {
exec(empty, empty);
});
// unhandled rejections tracking support, NodeJS Promise without it fails @@species test
return (
(isNode || typeof PromiseRejectionEvent == "function") &&
promise.then(empty) instanceof FakePromise
);
} catch (e) {
/* empty */
}
})();
// ...
複製程式碼
這個 USE_NATIVE
就是判定是否使用瀏覽器內建 Promise
的關鍵變數,果不其然,在 Firefox 中,這個值為 false
;再去 Chrome 檢視,發現值為 true
,我想,問題的根源已經找到了。
究其根源
為什麼 Firefox 中 USE_NATIVE
的值為 false
,通過跟蹤程式碼發現,關鍵點在於 PromiseRejectionEvent
這個介面,Firefox 中並沒有實現。
梳理一下,由於 Firefox 沒有實現 PromiseRejectionEvent
介面,導致 babel-polyfill
在判定是否使用 Promise
墊片函式時,認為當前執行環境是需要使用的,所以 Firefox 下的 Promise 被覆寫。然後因為 babel-transform-runtime
外掛的關係,為了避免全域性汙染,又將 Promise
做了模組化處理,也就是業務程式碼中的 Promise
全都使用的是被 babel-transform-runtime
模組化轉換後的 Promise
。
雖說是做了模組化處理 Promise
,那為什麼介面層 Promise
例項的屬性中會沒有 apiUid
?
表象是缺失了 apiUid
屬性,那就從介面請求的根源開始找原因,從最開始請求的呼叫函式開始除錯。
就當進入到 axios 原始碼除錯時發現:
axios 原始碼裡面使用的 Promise
是全域性的,也就是說我們業務程式碼內的 Promise
與 axios 使用的 Promise
不是同一個 Promise
,呃... ?
為什麼會這樣?還是得從根源思考,webpack 打包構建我們的業務程式碼,.js
檔案的處理都是通過 babel-loader
外掛,wait...wait...wait... axios 是屬於第三方依賴,檔案位置處於 node_modules
目錄下,babel-loader
肯定是做了 include
或 exclude
配置的,也就是說 axios 的原始碼並沒有被 babel-transform-runtime
做處理,嗦嘎... ?
*** webpack.base.conf.js ***
// ...
{
test: /\.js$/,
loader: 'babel-loader?cacheDirectory',
include: [
resolve('src'),
resolve('test'),
resolve('node_modules/webpack-dev-server/client'),
],
},
// ...
複製程式碼
OK,為什麼業務程式碼與 axios 原始碼使用的不是同一個 Promise
的原因已經找到了,那對我們的介面異常處理邏輯又有什麼影響了?
由於 axios 原始碼內的 Promise
沒有被 babel-transform-runtime
模組化處理,介面呼叫底層使用的也就是全域性的Promise
,也就是說被覆寫的 axios.Axios.prototype.request
函式返回的是全域性 Promise
的例項。
那其實介面呼叫時使用的 then
方法和 catch
方法都是全域性 Promise
的例項方法,與我們在 polyfill.js
內覆寫 Promise.prototype.then
方法和 Promise.prototype.catch
方法毫無瓜葛。理所當然,我們的異常狀態統一處理方案肯定就無法生效。
解決方案
問題的原因已經找到,那思考如何解決吧。
既然是 babel-polyfill
程式碼造成的問題,那移除 babel-polyfill
?
肯定不行,ES6+ 的例項方法怎麼辦?
那既然是 babel-transform-runtime
外掛將 Promise
做了模組化處理,那移除 babel-transform-runtime
?
也不行,這樣 babel 就會在每個打包後的檔案內插入重複相同的 helper 函式。
這樣不行,那也不行,到底咋整??
還是得從問題根源出發,我們需要 babel-transform-runtime
模組化 helper 程式碼,也需要 babel-polyfill
提供例項方法相容瀏覽器。這其中就有一個挺矛盾的問題,babel-transform-runtime
會模組化程式碼,而 babel-polyfill
又汙染全域性環境,應該怎麼解開這其中的糾葛?
其實這時候就需要我們弄清 babel-transform-runtime
和 babel-polyfill
它們的使用場景,它們是為了解決什麼問題而產生的。
babel-transform-runtime
的產生主要是為了解決 library 程式碼需要轉換成 ES5,但又不確定宿主環境,無法直接使用 babel-polyfill
;程式碼轉化過程中會產生一些 helper 函式,在多檔案的情況下就會在多個檔案內都新增 helper 函式,導致不必要的重複,所以進行模組化 helper 函式處理;為了不汙染全域性環境,會將 polyfill 和 regenerator 函式也進行模組化處理。
所以 babel-transform-runtime
主要解決 library 不確定宿主和 helper 程式碼模組化等相關的問題。
babel-polyfill
提供的就是完整的墊片函式(API、靜態方法、例項方法),以相容目前各家瀏覽器規範不統一的問題。
以上,就是 babel-transform-runtime
和 babel-polyfill
應用場景和產生背景。那針對我們系統而言,肯定是必須使用 babel-polyfill
的,因為 babel 無法轉譯例項方法,所以我們需要拿 babel-transform-runtime
開刀。
稍作思考,屢屢其中的關係就會發現,針對我們系統應用程式而言,需要 babel-transform-runtime
對 polyfill 進行模組化嗎?而且我們還必須使用 babel-polyfill
。
所以,解決方案就很清晰了,將 babel-transform-runtime
的 polyfill
配置設定為 false
即可。
*** .babelrc ***
// ...
"plugins": [
"transform-vue-jsx",
["transform-runtime", {
"polyfill": false
}]
]
// ...
複製程式碼
貌似 regenerator
函式也沒必要模組化,不過我們暫時不管它吧。
Over