接口异常状态统一处理方案在 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

相关文章
相关标签/搜索