blog
blog copied to clipboard
单页面应用批量取消请求的最佳实践
批量取消请求
在 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, 一个跨平台的网络请求库
参考文档
最近也在研究这个问题,不过没有考虑到路由变化的问题,只考虑到单页面连续操作的问题,最后写了一个deferer-queue的包,不过切换路由应该会有钩子暴露,在切换前cancel掉queue应该是可以的。
AbortController
https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort
不过我们项目用 RxJS,你的这个问题一个 takeUtil 就解决了。
@hjin-me 这个AbortController还是个试验性的接口,abort api其实XHR也有啊,我个人感觉是,使用太麻烦,如果一个请求实例能够主动提供一个cancel方法,那岂不是爽歪歪
RxJS也用过,确实可以用它自带的ajax方法,不需要考虑cancel动作,而是根据流的情况自动丢掉,很舒服。
axios.defaultSource ???? 有这个属性吗
解决问题了,感谢