接口異常狀態統一處理方案在 Firefox 下無效的緣由和解決方案

沒想到會是在雙十一這麼忙的時間段把這篇文章寫完 😂,公司很忙很緊張,可我還有時間在公司作分享,寫博文,慚愧慚愧... 作後臺系統在雙十一期間不如 2c 端的小夥伴有參與感呀。vue

banner

問題根源

上文 接口異常狀態統一處理方案:優先業務端處理,再按需統一處理。 最後提出可能存在的問題:node

若是項目是由 vue-cli 搭建的 webpack 模板項目,在沒有修改 .babelrc 文件配置的狀況下,此方案在 Firefox 瀏覽器下是無效的。接口狀態異常的狀況下,老是會執行統一處理,不會先交由業務端處理異常,再斷定是否執行統一處理。webpack

經過 debug 發現,handleAPIStatusError 函數老是在 catch 函數以前先執行,致使每次都能在 handleAPIStatusError 函數內找到未處理的異常接口的 apiUid。這就奇怪了,Promise 是 micro-task,setTimeout 是 macro-task,總不多是 EvenLoop 的問題吧 🙄,不可能不可能,想多了。💀
那究竟是什麼緣由呢?只能走代碼了,Go Go Go... 而後在 Firefox 開發者工具內,發如今 polyfill.jsPromise 的墊片函數 thencatch 內部,this 的屬性中竟然沒有 apiUid!並且還有一堆莫名其妙不明因此的屬性。😳
怎麼肥四?🤔ios

promise-no-apiUid

我表示看不懂,this 不該該是 Promise 的實例嗎?如今這個是個什麼鬼?💀git

通過一番掙扎,忽然發現:
「咦,Promise 哪去了?」
「那個 __WEBPACK_IMPORTED_MODULE_0_babel_runtime_core_js_promise___default 是什麼鬼?」
而後... 忽然醒悟 😠es6

babel_runtime_core_js_promise
core_jsbabel-polyfill
babel_runtimebabel-transform-runtime
難道 Firefox 內置的 Promise 是假的?
一臉驚悚... 😱github

帶着疑惑的心態去查找 babel-polyfill 的源碼,找到 Promise 的墊片函數:web

*** 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,我想,問題的根源已經找到了。vue-cli

究其根源

爲何 Firefox 中 USE_NATIVE 的值爲 false,經過跟蹤代碼發現,關鍵點在於 PromiseRejectionEvent 這個接口,Firefox 中並無實現。axios

caniuse-PromiseRejectionEvent

梳理一下,因爲 Firefox 沒有實現 PromiseRejectionEvent 接口,致使 babel-polyfill 在斷定是否使用 Promise 墊片函數時,認爲當前運行環境是須要使用的,因此 Firefox 下的 Promise 被覆寫。而後由於 babel-transform-runtime 插件的關係,爲了不全局污染,又將 Promise 作了模塊化處理,也就是業務代碼中的 Promise 全都使用的是被 babel-transform-runtime 模塊化轉換後的 Promise

雖然說是作了模塊化處理 Promise,那爲何接口層 Promise 實例的屬性中會沒有 apiUid

表象是缺失了 apiUid 屬性,那就從接口請求的根源開始找緣由,從最開始請求的調用函數開始調試。
就當進入到 axios 源碼調試時發現:

Axios.prototype.request

axios 源碼裏面使用的 Promise 是全局的,也就是說咱們業務代碼內的 Promise 與 axios 使用的 Promise 不是同一個 Promise,呃... 😳

爲何會這樣?仍是得從根源思考,webpack 打包構建咱們的業務代碼,.js 文件的處理都是經過 babel-loader 插件,wait...wait...wait... axios 是屬於第三方依賴,文件位置處於 node_modules 目錄下,babel-loader 確定是作了 includeexclude 配置的,也就是說 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-runtimebabel-polyfill 它們的使用場景,它們是爲了解決什麼問題而產生的。

babel-transform-runtime 的產生主要是爲了解決 library 代碼須要轉換成 ES5,但又不肯定宿主環境,沒法直接使用 babel-polyfill;代碼轉化過程當中會產生一些 helper 函數,在多文件的狀況下就會在多個文件內都添加 helper 函數,致使沒必要要的重複,因此進行模塊化 helper 函數處理;爲了避免污染全局環境,會將 polyfill 和 regenerator 函數也進行模塊化處理。
因此 babel-transform-runtime 主要解決 library 不肯定宿主和 helper 代碼模塊化等相關的問題。

babel-polyfill 提供的就是完整的墊片函數(API、靜態方法、實例方法),以兼容目前各家瀏覽器規範不統一的問題。

以上,就是 babel-transform-runtimebabel-polyfill 應用場景和產生背景。那針對咱們系統而言,確定是必須使用 babel-polyfill 的,由於 babel 沒法轉譯實例方法,因此咱們須要拿 babel-transform-runtime 開刀。
稍做思考,屢屢其中的關係就會發現,針對咱們系統應用程序而言,須要 babel-transform-runtime 對 polyfill 進行模塊化嗎?並且咱們還必須使用 babel-polyfill

因此,解決方案就很清晰了,將 babel-transform-runtimepolyfill 配置設置爲 false 便可。

*** .babelrc ***
// ...
"plugins": [
  "transform-vue-jsx",
  ["transform-runtime", {
    "polyfill": false
  }]
]
// ...
複製代碼

貌似 regenerator 函數也不必模塊化,不過咱們暫時無論它吧。

Over

相關文章
相關標籤/搜索