「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」。
写在开头
在上一篇文章 细读Axios源码系列三 - 拦截器,.use(fn, fn) 方法实现、同步与异步拦截器、移除拦截器 中,我们学完了 axios
中非常重要的核心功能 - 拦截器。那么,本章我们继续来学习它的另一个核心功能 "取消请求" 。
这里,小编唠叨一下,希望感兴趣观看了这个系列的小伙伴们再坚持坚持,axios
源码内容不是很多,大概再有两三篇文章,我们就基本上把 axios
源码完整的讲完了,胜利就在眼前,加油哈。当然,这里也自己鼓励下自己,坚持一定要写完。
预备知识
用json-server模块制造一个5秒请求
在第一文章 细读Axios源码系列一 - 从零搭建项目架构,项目准备、项目打包、项目测试流程 中,我们讲过如何通过 json-server 模块快速启动一个网络服务。
现在我们来讲一下,如何通过这个模块制造一个超长时长的请求,其实很简单,只要在执行启动命令时,添加 -d xxx
参数即可:
json-server --watch db.json -d 5000
复制代码
我们可以在浏览器访问 http://localhost:3000/posts
接口,查看控制台:
这就简单完成一个时长比较久的请求了,这方便我们后续测试 "取消请求" 功能。
了解XMLHttpRequest.abort()方法
XMLHttpRequest 对象咱就不多介绍了,前端人的必备知识,我们来看看它的 .abort() 方法,应该有不少人没接触过它。
文档上关于它的介绍很简单:
下面,我们来看一个小例子应该就明白了:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<button onclick="sendHandle()">发送请求</button>
<button onclick="cancelHandle()">取消请求</button>
<script>
var xhr;
function sendHandle() {
xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:3000/posts', true);
xhr.send(null);
}
function cancelHandle() {
xhr.abort();
}
</script>
</body>
</html>
复制代码
当我们点击 "发送请求" 正常情况,5秒后我们就能拿到请求结果:
但是,当我们5秒内,点击 "取消请求" ,那么这个请求你就获取不到结果,请求已经被 取消 了。
这就是 .abort()
方法的作用,很简单也很好理解吧。当然,也有需要注意的地方,对于前端来说,这个请求已经被取消了,前端不会再收到后端返回的任何结果;但是,对于后端来说,这个请求在前端已经是发送出去了,所以后端还是能接到这个请求的,这是一个需要我们注意的地方。
取消请求
花了一点时间讲了一些预备知识,其实,聪明的小伙伴应该已经猜到了, .abort()
方法就是本章的核心,我们其实就是来学一下 axios
是如何把 .abort()
方法给封装进去的,让我们使用起来更方便。
基本使用
根据 axios使用文档 的介绍,axios
的 "取消请求" 功能有两种用法。
用法一:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<button onclick="sendHandle()">发送请求</button>
<button onclick="cancelHandle()">取消请求</button>
<script>
const CancelToken = axios.CancelToken;
let cancel;
function sendHandle() {
axios({
url: 'http://localhost:3000/posts',
method: 'get',
cancelToken: new CancelToken(function executor(c) {
cancel = c;
})
}).then(res => {
console.log(res);
}).catch(err => {
if (axios.isCancel(err)) {
console.log(err.message); // 这个请求已经被取消
}
})
}
function cancelHandle() {
cancel('这个请求已经被取消');
}
</script>
</body>
</html>
复制代码
用法二:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<button onclick="sendHandle()">发送请求</button>
<button onclick="cancelHandle()">取消请求</button>
<script>
let CancelToken = axios.CancelToken;
const source = CancelToken.source();
function sendHandle() {
axios({
url: 'http://localhost:3000/posts',
method: 'get',
cancelToken: source.token
}).then(res => {
console.log(res);
}).catch(err => {
if (axios.isCancel(err)) {
console.log(err.message); // 这个请求已经被取消
}
})
}
function cancelHandle() {
source.cancel('这个请求已经被取消');
}
</script>
</body>
</html>
复制代码
了解完基本使用后,下面我们根据这两个例子来慢慢完善我们自己的 axios
代码。
CancelToken方法的创建
根据上面的例子,我们先来把 axios.CancelToken
方法创建出来,并把它挂载在 axios
对象身上,来到我们的 axios.js
文件:
// lib/axios.js
var Axios = require('./core/Axios');
var bind = require('./helpers/bind');
var utils = require('./utils');
function createInstance() {
var context = new Axios();
var instance = bind(Axios.prototype.request, context);
utils.extend(instance, Axios.prototype, context);
utils.extend(instance, context);
return instance;
}
var axios = createInstance();
// 取消请求相关逻辑
axios.CancelToken = require('./cancel/CancelToken');
axios.Cancel = require('./cancel/Cancel');
axios.isCancel = require('./cancel/isCancel');
module.exports = axios;
复制代码
上面代码中,我们引入了三个新文件,因为都是在一个文件夹下的,也都和 "取消请求" 功能相关,这里就先一并引入了,我们创建这三个文件。 CancelToken.js
文件中,我们创建两个方法:
// lib/cancel/CancelToken.js
var Cancel = require('./Cancel');
/**
* 取消请求的操作: 该函数在使用时, 会先被实例化, 会在 this 身上储存很多东西, 等待被传递到 xhr.js 文件中被调用使用.
* @param {Function} executor: 外部使用时需要传递进来的回调函数, 当我们内部执行 executor 函数时, 会传递另一个函数作为参数, 函数的内容主要是去执行 abort() 方法
*/
function CancelToken(executor) {}
CancelToken.source = function source() {};
module.exports = CancelToken;
复制代码
其他另外两个文件,我们先暂时不写任何内容。
然后我们执行 grunt build
命令把项目打包,再把上方 "用法一" 中的例子修改,引入我们自己的 axios
包:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="../dist/axios.js"></script>
</head>
...
复制代码
这个时候页面不会报错,这是我们要做的第一步。
然后我们来到 lib/adapters/xhr.js
文件:
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var request = new XMLHttpRequest();
request.open(config.method.toUpperCase(), config.url, true);
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) return;
resolve(request.response);
request = null; // 请求结束
};
// 取消请求
if (config.cancelToken) {
// 等等 promise 被执行, 然后就执行 取消请求 方法
config.cancelToken.promise.then(function onCanceled(cancel) {
// 因为是异步执行, 如果取消在请求已经结束前就完成, 就没必要执行取消
if (!request) return;
request.abort(); // 取消请求
reject(cancel); // cancel 为 "取消对象"
request = null;
});
}
request.send();
});
}
复制代码
上面代码通过判断 cancelToken
属性是否有传入来执行 "取消请求" 的逻辑。注意 config
参数就是我们使用 axios(config)
对象时传递的配置参数,config
参数的传递过程是比较复杂的,它主要会经过这些文件: Axios.js
=> dispatchRequest.js
=> xhr.js
。
我们回到 lib/cancel/CancelToken.js
文件:
var Cancel = require('./Cancel');
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('CancelToken 接收的参数必须是一个参数');
}
// 实例化一个 Promise 对象, 然后把这个 Promise 挂载在 CancelToken 方法身上暴露出去
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
// 把 this 重写成 token 会更加形象, 因为本身每个请求就是需要一个唯一的 token 来标识
var token = this;
/**
* 执行外部传递的 executor(c) 方法, 并传递出另一个函数作为参数
* @param {Function} cancel: 暴露给 "外部" 手动执行 promise 的方法
*/
executor(function cancel(message) {
// 已经取消了的请求会在 reason 做一个标识
if (token.reason) return;
// 请求取消后, 生成取消对象
token.reason = new Cancel(message);
// 执行 promise 的 resolve() 并把 "取消对象" 传递出去, 方便后续外部请求判断
resolvePromise(token.reason);
});
}
/**
* 这个方法其实为了对应 "用法二" 的情况, 其实它就是把手动实例化这步骤放在内部,通过暴露一个方法给外部去执行
*/
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
module.exports = CancelToken;
复制代码
lib/cancel/Cancel.js
文件:
/**
* "取消对象": 请求被取消后生成的一个对象, 这个会存储这个请求被取消时传递的一些信息
* @class
* @param {string=} message: 手动执行 cancel(message) 中的 message信息, 如例子中的 "这个请求已经被取消"
*/
function Cancel(message) {
this.message = message;
}
/**
* 在取消对象身上挂载 toString() 方法, 方便我们直接查询 取消对象 的信息
* 例如: var o = new Object(); o.toString(); => [object Object]
*/
Cancel.prototype.toString = function toString() {
return 'Cancel' + (this.message ? ': ' + this.message : '');
};
// 在取消对象原型上做一个取消标识, 方法后续的判断
Cancel.prototype.__CANCEL__ = true;
module.exports = Cancel;
复制代码
上面代码中,小编把能写上注释的地方都写上了注释,希望你能读懂哈,这两个文件就是 axios
中 "取消请求" 功能的核心代码了。
总的来说,这部分逻辑很绕,第一次读它的时候,小编自己也很懵逼来着,但是,值得肯定的是 axios
这个功能设计得真的非常巧妙和精细的,这个过程的设计思想是值得我们去学习和反复推敲的。
为了让你更好理解一点,小编把它单独拎了出来,希望的小伙伴可以看看:
let axios = function(config) {
config.cancelToken.promise.then(cancel => {
console.log('取消请求')
});
}
axios.CancelToken = function(executor) {
var resolvePromise;
this.promise = new Promise((resolve) => {
resolvePromise = resolve;
});
executor(() => { resolvePromise() })
}
const CancelToken = axios.CancelToken;
let cancel;
axios({
cancelToken: new CancelToken(c => { cancel = c })
})
setTimeout(() => {
cancel();
}, 3000)
复制代码
再附上一张执行过程图:
其他小细节
完成上面这一步骤后,其实 "取消请求" 的功能大体逻辑就算写完了,剩下一点细节,我们把它补充完整。
lib/cancel/isCancel.js
文件:
/**
* 判断是否是一个被取消了的请求
* @param {*} value
* @returns
*/
module.exports = function isCancel(value) {
return !!(value && value.__CANCEL__);
};
复制代码
这个方法是提供给 axios
外部用于判断某个请求是否是一个被取消了的请求。判断标识 __CANCEL__
是在 lib/cancel/Cancel.js
文件中被标识上的。
使用过程如下:
我们再来到 lib/core/dispatchRequest.js
文件中:
var defaults = require('../defaults'); // 引入新文件
var isCancel = require('../cancel/isCancel');
/**
* 过滤已取消的请求
* @param {*} config
*/
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
module.exports = function dispatchRequest(config) {
// 这个过滤触发的时机是: axios是单例的时候,如果前一个请求被取消, 那会在它原型上标识 __CANCEL__
// 即使修改了 url 等参数形式, 但依旧用这个实例发起网络请求, 还是会被过滤掉
throwIfCancellationRequested(config); // 过滤已取消的请求
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
// 这个过滤触发的时机是: 请求已经发出, 但已经获取到结果, 准备正常返回时, 又将请求取消了的时候
throwIfCancellationRequested(config); // 过滤已取消的请求
return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
// 这个过滤触发的时机是: 请求已经发出, 但获取结果时出现异常, 如网络情况、超时等, 准备返回异常信息时, 又将请求取消了的时候
throwIfCancellationRequested(config); // 过滤已取消的请求
}
return Promise.reject(reason);
});
}
复制代码
不知道你是否还记得这个文件,忘记了的小伙伴可以回过头来看看第二篇 细读Axios源码系列二 - axios对象创建、request核心方法、发起网络请求 文章。
这个文件被执行,就等于会执行 lib/adapters/xhr.js
文件,也就等于会发送起网络请求,所以它是发送网络请求的最后一关卡,也是网络请求结束后的第一个关卡。我们需要在每个网络请求,发送前与结束后做一个过滤,如果是已经被取消了的请求,一律统一抛出错误,不给前端返回任何结果。
那么,写到这里和取消请求功能相关的逻辑就都写完啦,最后,要记得执行 grunt build
打包命令,测试一下我们写在上方的两个例子用法,如果使用正常并且没有报错,就说明你成功啦。
至此,本篇文章就写完啦,撒花撒花。
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。