{"action":"created","issue":{"url":"https://api.github.com/repos/chunpu/blog/issues/98","repository_url":"https://api.github.com/repos/chunpu/blog","labels_url":"https://api.github.com/repos/chunpu/blog/issues/98/labels{/name}","comments_url":"https://api.github.com/repos/chunpu/blog/issues/98/comments","events_url":"https://api.github.com/repos/chunpu/blog/issues/98/events","html_url":"https://github.com/chunpu/blog/issues/98","id":372515961,"node_id":"MDU6SXNzdWUzNzI1MTU5NjE=","number":98,"title":"单页面应用批量取消请求的最佳实践","user":{"login":"chunpu","id":4565306,"node_id":"MDQ6VXNlcjQ1NjUzMDY=","avatar_url":"https://avatars2.githubusercontent.com/u/4565306?v=4","gravatar_id":"","url":"https://api.github.com/users/chunpu","html_url":"https://github.com/chunpu","followers_url":"https://api.github.com/users/chunpu/followers","following_url":"https://api.github.com/users/chunpu/following{/other_user}","gists_url":"https://api.github.com/users/chunpu/gists{/gist_id}","starred_url":"https://api.github.com/users/chunpu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/chunpu/subscriptions","organizations_url":"https://api.github.com/users/chunpu/orgs","repos_url":"https://api.github.com/users/chunpu/repos","events_url":"https://api.github.com/users/chunpu/events{/privacy}","received_events_url":"https://api.github.com/users/chunpu/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":2,"created_at":"2018-10-22T13:13:49Z","updated_at":"2018-11-29T04:16:42Z","closed_at":null,"author_association":"OWNER","body":"## 批量取消请求\r\n\r\n在 Vue, React 等前端超级框架横行的时代, 越来越多的网页变成了SPA\r\n\r\nSPA, 水疗养生保健, 哦不, 单页面应用, 一个网页就是一个app, 路由被前端代理\r\n\r\n后端新来的小哥哥一脸懵逼, 为什么我点新链接不请求服务端 html 了?\r\n\r\n每次切换到新的路由, 只需请求 json 数据就可以展示对应的视图, 简直美滋滋\r\n\r\n好日子没过几天, 测试小姐姐隔着三排工位喊道: 前端你的内容是不是搞混了??\r\n\r\n反复在几个路由中切换, 内容总是被互相覆盖, 文不对题, 前端小哥哥也懵逼了\r\n\r\n简单调试一下\r\n\r\n**路由虽然被切换, 但请求还在发! 回调还在执行! Promise 还在半路上!**\r\n\r\n需求变成了: 每次切换路由都要取消当前路由下的全部请求\r\n\r\n看着满屏幕的 `GET` `POST` 请求, 前端小哥痛不欲生, 为什么要用前端路由?\r\n\r\n前端小哥甚至有点怀念古典Web, 那时候, 我们一点链接一跳转就进入一个新的页面, 哪还管洪水滔天\r\n\r\n没办法, 改\r\n\r\n前端小哥一开始用的是 `fetch`, 天然提供了 Promise 接口, 串来串去就像过山路十八弯, 非常顺手\r\n\r\n但一查 MDN, fetch 竟然不支持取消请求! WTF\r\n\r\n一个如此现代的 API, 竟然不支持取消请求, 简直还不如 `XMLHttpRequest`, 人家十八年前就可以 abort 了, 哭唧唧\r\n\r\n不过转念一想, Promise 自己的 cancel 标准八字还没一撇呢, 不支持也正常\r\n\r\n没办法, 前端小哥决定改用现在请求库中的当红炸子鸡, `axios`, 一个可以在浏览器和 Node.js 的 http 请求库, 而且还支持 `Cancel requests`, 简直棒棒哒\r\n\r\n## axios 的 CancelToken\r\n\r\n打开 axios 的[文档](https://github.com/axios/axios#cancellation), 好嘛!(天津话版), axios 为了在 Promise 中实现取消的效果, 也算是费尽周折\r\n\r\naxios 使用了已经被撤回的可取消的 Promise 标准提案 (都撤回了你还用?)\r\n\r\n使用方法大概是这样(来自官方)\r\n\r\n```js\r\nconst CancelToken = axios.CancelToken;\r\nconst source = CancelToken.source();\r\n\r\naxios.get('/user/12345', {\r\n  cancelToken: source.token\r\n}).catch(function (thrown) {\r\n  if (axios.isCancel(thrown)) {\r\n    console.log('Request canceled', thrown.message);\r\n  } else {\r\n    // handle error\r\n  }\r\n});\r\n\r\naxios.post('/user/12345', {\r\n  name: 'new name'\r\n}, {\r\n  cancelToken: source.token\r\n})\r\n\r\n// cancel the request (the message parameter is optional)\r\nsource.cancel('Operation canceled by the user.');\r\n```\r\n\r\n如果你看不懂, 没关系, 我一开始也没看懂, 甚至要有点小气愤\r\n\r\n为什么取消一个请求我们要 `source`, `CancelToken`, `cancel` 这些东西呢? 前端小哥表示怀念 `xhr.abort()` 的时代\r\n\r\n## 如何从外部取消一个 Promise?\r\n\r\n我们不妨先看看 axios 中的 adapter 是如何实现这个 cancel 的\r\n\r\n`axios/lib/adapters/http.js`\r\n\r\n```js\r\nif (config.cancelToken) {\r\n  // Handle cancellation\r\n  config.cancelToken.promise.then(function onCanceled(cancel) {\r\n    if (req.aborted) return;\r\n\r\n    req.abort();\r\n    reject(cancel);\r\n  });\r\n}\r\n```\r\n\r\ncancelToken 内置了一个 promise 对象, 而且可以从外部结束这个 promise, 这个结构感觉有点眼熟\r\n\r\n**恍然大悟! 这不就是 Deferred 吗!**\r\n\r\nDeferred, 是 Promise 的一种扩展, 提供了可以手动改变 Promise 状态的方法\r\n\r\n我们甚至可以模仿 Deferred 快速实现一个 `canceltoken.js`\r\n\r\n```js\r\nmodule.exports = Deferred\r\n\r\nfunction Deferred() {\r\n  this.promise = new Promise((resolve, reject) => {\r\n    this.resolve = resolve\r\n    this.reject = reject\r\n  })\r\n}\r\n\r\nDeferred.source = () => {\r\n  var deferred = new Deferred()\r\n  return {\r\n    token: deferred, // token 就是 deferred\r\n    cancel (reason) { // cancel 就是 resolve\r\n      deferred.resolve(new Error(reason))\r\n    }\r\n  }\r\n}\r\n\r\nDeferred.prototype.throwIfRequested = function() {\r\n  // 兼容 axios\r\n}\r\n```\r\n\r\n## 如何批量取消请求?\r\n\r\n前端小哥百度了一下路由跳转如何批量取消请求, 排名最前面的结果是这样建议的:\r\n\r\n> ![](https://p2.ssl.qhimg.com/t012fbfd845a03ba023.jpg)\r\n\r\n在一个全局数组中保存所有 axios 请求显然是不可接受的\r\n\r\n如果你了解 Promise 的一些原理, 就可以明白 `.then` 函数的本质是传宗接代, 开枝散叶\r\n\r\n> 当一个 token 被 cancel, 其相关的所有后代 promise 都会被执行\r\n\r\n也就是说 axios **天然就自带**了批量取消请求的功能!\r\n\r\n## 批量取消请求最终代码实现\r\n\r\n`@/util.js`\r\n\r\n```js\r\nimport axios from 'axios'\r\n\r\naxios.defaultSource = axios.CancelToken.source()\r\n\r\naxios.interceptors.request.use(config => {\r\n  // 默认给请求加上 cancelToken (不包含 null)\r\n  if (config.cancelToken === undefined) {\r\n    config.cancelToken = axios.defaultSource.token\r\n  }\r\n  return config\r\n})\r\n```\r\n\r\n`@/router.js`\r\n\r\n```js\r\nimport axios from 'axios'\r\n\r\nrouter.beforeEach((to, from, next) => {\r\n  axios.defaultSource.cancel('切换路由取消请求')\r\n  axios.defaultSource = axios.CancelToken.source() // 刷新 defaultSource\r\n  next()\r\n})\r\n```\r\n\r\n对于不想被取消的请求, 比如请求**用户信息**\r\n\r\n```js\r\nimport axios from 'axios'\r\n\r\naxios.get('/user/info', {\r\n  cancelToken: null // 指明不想要 cancelToken 就可以啦\r\n})\r\n```\r\n\r\n## 在其他平台批量取消请求\r\n\r\n如果你还希望在小程序, 快应用等其他平台中像 axios 那样批量取消请求\r\n\r\n也可以使用 [@chunpu/http](https://github.com/chunpu/http), 一个跨平台的网络请求库\r\n\r\n参考文档\r\n\r\n- <https://blog.csdn.net/qq_24833847/article/details/82454762>\r\n"},"comment":{"url":"https://api.github.com/repos/chunpu/blog/issues/comments/442700727","html_url":"https://github.com/chunpu/blog/issues/98#issuecomment-442700727","issue_url":"https://api.github.com/repos/chunpu/blog/issues/98","id":442700727,"node_id":"MDEyOklzc3VlQ29tbWVudDQ0MjcwMDcyNw==","user":{"login":"tangshuang","id":7940461,"node_id":"MDQ6VXNlcjc5NDA0NjE=","avatar_url":"https://avatars0.githubusercontent.com/u/7940461?v=4","gravatar_id":"","url":"https://api.github.com/users/tangshuang","html_url":"https://github.com/tangshuang","followers_url":"https://api.github.com/users/tangshuang/followers","following_url":"https://api.github.com/users/tangshuang/following{/other_user}","gists_url":"https://api.github.com/users/tangshuang/gists{/gist_id}","starred_url":"https://api.github.com/users/tangshuang/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tangshuang/subscriptions","organizations_url":"https://api.github.com/users/tangshuang/orgs","repos_url":"https://api.github.com/users/tangshuang/repos","events_url":"https://api.github.com/users/tangshuang/events{/privacy}","received_events_url":"https://api.github.com/users/tangshuang/received_events","type":"User","site_admin":false},"created_at":"2018-11-29T04:16:42Z","updated_at":"2018-11-29T04:16:42Z","author_association":"NONE","body":"@hjin-me 这个AbortController还是个试验性的接口，abort api其实XHR也有啊，我个人感觉是，使用太麻烦，如果一个请求实例能够主动提供一个cancel方法，那岂不是爽歪歪\r\n\r\nRxJS也用过，确实可以用它自带的ajax方法，不需要考虑cancel动作，而是根据流的情况自动丢掉，很舒服。"}}
{"actor":{"display_login":"tangshuang"}}