chunpu / blog

personal blog render by jekyll
MIT License
51 stars 8 forks source link

单页面应用批量取消请求的最佳实践 #98

Open chunpu opened 5 years ago

chunpu commented 5 years ago

批量取消请求

在 Vue, React 等前端超级框架横行的时代, 越来越多的网页变成了SPA

SPA, 水疗养生保健, 哦不, 单页面应用, 一个网页就是一个app, 路由被前端代理

后端新来的小哥哥一脸懵逼, 为什么我点新链接不请求服务端 html 了?

每次切换到新的路由, 只需请求 json 数据就可以展示对应的视图, 简直美滋滋

好日子没过几天, 测试小姐姐隔着三排工位喊道: 前端你的内容是不是搞混了??

反复在几个路由中切换, 内容总是被互相覆盖, 文不对题, 前端小哥哥也懵逼了

简单调试一下

路由虽然被切换, 但请求还在发! 回调还在执行! Promise 还在半路上!

需求变成了: 每次切换路由都要取消当前路由下的全部请求

看着满屏幕的 GET POST 请求, 前端小哥痛不欲生, 为什么要用前端路由?

前端小哥甚至有点怀念古典Web, 那时候, 我们一点链接一跳转就进入一个新的页面, 哪还管洪水滔天

没办法, 改

前端小哥一开始用的是 fetch, 天然提供了 Promise 接口, 串来串去就像过山路十八弯, 非常顺手

但一查 MDN, fetch 竟然不支持取消请求! WTF

一个如此现代的 API, 竟然不支持取消请求, 简直还不如 XMLHttpRequest, 人家十八年前就可以 abort 了, 哭唧唧

不过转念一想, Promise 自己的 cancel 标准八字还没一撇呢, 不支持也正常

没办法, 前端小哥决定改用现在请求库中的当红炸子鸡, axios, 一个可以在浏览器和 Node.js 的 http 请求库, 而且还支持 Cancel requests, 简直棒棒哒

axios 的 CancelToken

打开 axios 的文档, 好嘛!(天津话版), axios 为了在 Promise 中实现取消的效果, 也算是费尽周折

axios 使用了已经被撤回的可取消的 Promise 标准提案 (都撤回了你还用?)

使用方法大概是这样(来自官方)

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

如果你看不懂, 没关系, 我一开始也没看懂, 甚至要有点小气愤

为什么取消一个请求我们要 source, CancelToken, cancel 这些东西呢? 前端小哥表示怀念 xhr.abort() 的时代

如何从外部取消一个 Promise?

我们不妨先看看 axios 中的 adapter 是如何实现这个 cancel 的

axios/lib/adapters/http.js

if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (req.aborted) return;

    req.abort();
    reject(cancel);
  });
}

cancelToken 内置了一个 promise 对象, 而且可以从外部结束这个 promise, 这个结构感觉有点眼熟

恍然大悟! 这不就是 Deferred 吗!

Deferred, 是 Promise 的一种扩展, 提供了可以手动改变 Promise 状态的方法

我们甚至可以模仿 Deferred 快速实现一个 canceltoken.js

module.exports = Deferred

function Deferred() {
  this.promise = new Promise((resolve, reject) => {
    this.resolve = resolve
    this.reject = reject
  })
}

Deferred.source = () => {
  var deferred = new Deferred()
  return {
    token: deferred, // token 就是 deferred
    cancel (reason) { // cancel 就是 resolve
      deferred.resolve(new Error(reason))
    }
  }
}

Deferred.prototype.throwIfRequested = function() {
  // 兼容 axios
}

如何批量取消请求?

前端小哥百度了一下路由跳转如何批量取消请求, 排名最前面的结果是这样建议的:

在一个全局数组中保存所有 axios 请求显然是不可接受的

如果你了解 Promise 的一些原理, 就可以明白 .then 函数的本质是传宗接代, 开枝散叶

当一个 token 被 cancel, 其相关的所有后代 promise 都会被执行

也就是说 axios 天然就自带了批量取消请求的功能!

批量取消请求最终代码实现

@/util.js

import axios from 'axios'

axios.defaultSource = axios.CancelToken.source()

axios.interceptors.request.use(config => {
  // 默认给请求加上 cancelToken (不包含 null)
  if (config.cancelToken === undefined) {
    config.cancelToken = axios.defaultSource.token
  }
  return config
})

@/router.js

import axios from 'axios'

router.beforeEach((to, from, next) => {
  axios.defaultSource.cancel('切换路由取消请求')
  axios.defaultSource = axios.CancelToken.source() // 刷新 defaultSource
  next()
})

对于不想被取消的请求, 比如请求用户信息

import axios from 'axios'

axios.get('/user/info', {
  cancelToken: null // 指明不想要 cancelToken 就可以啦
})

在其他平台批量取消请求

如果你还希望在小程序, 快应用等其他平台中像 axios 那样批量取消请求

也可以使用 @chunpu/http, 一个跨平台的网络请求库

参考文档

tangshuang commented 5 years ago

最近也在研究这个问题,不过没有考虑到路由变化的问题,只考虑到单页面连续操作的问题,最后写了一个deferer-queue的包,不过切换路由应该会有钩子暴露,在切换前cancel掉queue应该是可以的。

hjin-me commented 5 years ago

AbortController

https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort

不过我们项目用 RxJS,你的这个问题一个 takeUtil 就解决了。

tangshuang commented 5 years ago

@hjin-me 这个AbortController还是个试验性的接口,abort api其实XHR也有啊,我个人感觉是,使用太麻烦,如果一个请求实例能够主动提供一个cancel方法,那岂不是爽歪歪

RxJS也用过,确实可以用它自带的ajax方法,不需要考虑cancel动作,而是根据流的情况自动丢掉,很舒服。

Yunnkii commented 4 years ago

axios.defaultSource ???? 有这个属性吗

fliu2476 commented 4 years ago

解决问题了,感谢