介面異常狀態統一處理方案在 Firefox 下無效的原因和解決方案

Monine發表於2018-11-09

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

banner

問題根源

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

如果專案是由 vue-cli 搭建的 webpack 模板專案,在沒有修改 .babelrc 檔案配置的情況下,此方案在 Firefox 瀏覽器下是無效的。介面狀態異常的情況下,總是會執行統一處理,不會先交由業務端處理異常,再判定是否執行統一處理。

通過 debug 發現,handleAPIStatusError 函式總是在 catch 函式之前先執行,導致每次都能在 handleAPIStatusError 函式內找到未處理的異常介面的 apiUid。這就奇怪了,Promise 是 micro-task,setTimeout 是 macro-task,總不可能是 EvenLoop 的問題吧 ?,不可能不可能,想多了。?
那到底是什麼原因呢?只能走程式碼了,Go Go Go... 然後在 Firefox 開發者工具內,發現在 polyfill.jsPromise 的墊片函式 thencatch 內部,this 的屬性中居然沒有 apiUid!而且還有一堆莫名其妙不明所以的屬性。?
怎麼肥四??

promise-no-apiUid

我表示看不懂,this 不應該是 Promise 的例項嗎?現在這個是個什麼鬼??

經過一番掙扎,突然發現:
“咦,Promise 哪去了?”
“那個 __WEBPACK_IMPORTED_MODULE_0_babel_runtime_core_js_promise___default 是什麼鬼?”
然後... 突然醒悟 ?

babel_runtime_core_js_promise
core_jsbabel-polyfill
babel_runtimebabel-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 中並沒有實現。

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

相關文章