前言
axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。如今随着 vuejs 作者尤雨溪的推荐,已经被很多前端开发者熟知,并在很多前端项目中也得到运用,成为很多项目中首选的 HTTP 库。
axios主要有如下功能:
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
大家对这些功能应该或多或少有所了解。在本文中我想主要聊一下 axios 的 CancelToken,也就是取消请求的功能,总结一下该功能在我实际开发中的一些运用。
取消请求的基本用法和个人理解
在实际业务中,我们可能处于某种原因,需要随时取消已发送出去的请求,axios的官方文档给了两种取消请求的方法:
可以使用 CancelToken.source 工厂方法创建 cancel token,像这样:
var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
还可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token:
var CancelToken = axios.CancelToken;
var cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});
// 取消请求
cancel();
在上面两个例子中,分别执行 source.cancel 和 cancel 方法,都可以达到取消这个请求的目的。
那么这里可能有人要问:这两个例子区别在哪?分别适用在什么场景下使用?我们不妨看看源码,先从 axios.CancelToken 来看。这部分的源码在 axios/lib/cancel/CancelToken.js 文件中。
在上述第一个例子中,用到了一个 CancelToken.source 方法,我们来看一下这个方法:
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
source 方法看起来很清晰,其中通过 CancelToken 构造函数生成一个实例 token,同时在构造函数执行的过程中,赋值了一个变量 cancel,最后返回了一个拥有 token 和 cancel 属性的对象。
我们再接着看一下 CancelToken 这个构造函数:
function CancelToken(executor) {
//...
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
可以看出生成的实例 token 是一个对象,该对象有一个属性 promise,是一个处于 pending 状态的 Promise 实例。而 executor 方法里的参数 cancal 就是上述第一个例子中的 source.cancel 和第二个例子中的 cancel 方法,同时也就是 source 方法中的 cancel,而执行它就可以触发取消请求。
我们看 cancel 方法,给token加了一个 reason 属性,看一下 Cancel 构造函数:
function Cancel(message) {
this.message = message;
}
很明显,reason 就是一个含有 message 属性的对象。继续看 cancel 方法,resolvePromise(token.reason) 接着让 token.promise 变成了 resolved 的状态。
那么 token.promise 变为 resolved 之后,是怎么出触发取消请求的呢,我们可以看 axios/lib/adapters/xhr.js 这个文件,其中有一段:
//...
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
这一段方法,只有在配置了 cancelToken 并且 cancelToken.promise 的状态变成 resolved 后才会执行。而刚才我们分析了,cancel 方法就是让这个 promise 的状态变成 resolved。所以如果执行了取消请求的 cancel 方法,这个 then 就会执行,进一步取消发送请求,并且把发送请求的 promise 变成 reject,被 axiox.catch() 捕获。
到此,整个取消请求的流程已经清楚了,最后再总结一下: 执行 cancel 方法是让 token 的 promise 变成 resolved,这是就会触发取消请求。
回到之前的那个问题,官方文档中的这两个例子区别在哪?分别适用在什么场景下使用?
对于区别,通过上面这段源码分析,很容易看出这两个例子并没有本质区别,第二个例子中的 new CancelToken(...) 就是第一个例子中 CancelToken.source() 的一部分。区别就是第二个例子是在请求的配置项里单例生成一个 token ,并把 cancel 方法赋值到一个外部变量中;而第一个例子是通过一个工厂方法 CancelToken.source 创建一个对象 source:{token,cancel} 然后在具体的请求配置里引用它。
对于使用场景,我首先注意到官方文档的一个 Note:
可以使用同一个 cancel token 取消多个请求。
很显然,第一个例子里的 source 可以很方便的被多个请求配置来引用,如果多个请求同时使用这一个 source.token,我们在调用 source.cancel 的时候,就会同时取消这些请求。
所以,我理解的使用场景总结来说就是:
- 当业务中需要根据时机取消某一个请求时,用第二个例子的方法单例生成 token 就可以了。
- 当业务中需要根据时机取消多个请求时,建议使用第一个例子中的方法,生成一个外部的 token,让这些请求配置同时使用,然后可以通过一个 cancel 方法全部取消。
配合拦截器进行全局封装
在实际项目中,我们可能需要这些场景:
- 提交按钮的重复点击,表格分页器快速切换等操作会短时间发出很多重复请求。一方面有很多不必要的请求响应,另一方面也容易出现错误,比如快速点击分页器的第一页和第二页,请求发送的顺序是先发第一页,再发第二页,但结果由于服务端的某些原因,第二页的数据先返回,再返回第一页的。这时在正常的逻辑下,可能页面就会出现错误,就是会出现分页器显示在第二页,但表格数据渲染的是第一页的现象。
- 当路由跳转的时候,当前组件里有些请求已经发了出去但还没有得到响应,这时候很可能也不需要这些数据了,大多数情况下,我们希望中止这些请求。特别是在 React 项目中,当组件卸载时,如果组件中还有一些异步操作没有结束,会有一个警告 Warning:Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
对于这两个场景,我们可以通过利用拦截器和取消请求来对 axios 进行一些全局封装,下面进行详细介绍。
解决重复请求的问题
刚才说到的某些操作会短时间触发很多不必要重复请求的问题,可以通过很多方法解决。比如:给发送请求的方法添加防抖,在请求时禁用操作按钮,通过一个全局变量记住请求序号等等,在这里不多阐述绍。这里主要介绍一下通过对 axios 的封装来解决这个问题的方法。
思路如下:在建立每一个 axios 请求时,都给它配置一个 cancelToken。每次当有新的请求需要建立的时候,先判断一下有没有正在进行中的重复请求,如果有的话,就调用它的 cancel 方法将它终止,并建立新的请求。
基于这个思路来看一下代码,下面是我在一个 Vue 项目中全局封装 axios 的部分。首先,我在入口 main.js 文件中将 axios 绑定到 Vue 的实例属性 $http 上:
//main.js
Vue.prototype.$http = axios.create({
//...一些配置
});
接下来,我在根组件 App.vue 中对 axios 添加了拦截器:
//App.vue
export default {
created() {
//...其他操作
this.setAxiosInterceptors();
},
methods: {
setAxiosInterceptors() {
this.$http.interceptors.request.use(request => {
//...一些对请求的拦截操作
return request;
});
this.$http.interceptors.response.use(
response => {
//...一些对响应的拦截操作
return response;
},
error => {
//...一些错误处理
}
);
}
}
}
我是根据请求的 url 和 method 这个两个属性来共同判断是否为重复请求。首先我们需要定义一个数组,来保存当前正在执行的请求列表,这里在 data 里定义了一个 requestList:
//App.vue
data() {
return {
//当前正在执行的请求列表
requestList: [],
};
},
接下来,需要对请求和响应做拦截,按照刚才的思路分以下几个详细步骤:
- 在建立请求时,先用单例模式配置一个独有的 cancelToken,添加到 request.cancelToken 配置上;
- 再把 cancel 方法赋值到 request.cancel,供之后调用;
- 考虑到一些特殊情况,有部分请求可能就是需要同时发送,我们可以配置一个可重复请求的白名单列表;
- 如果当前请求不在白名单列表中,就通过 requestList 判断有没有重复请求,如果有重复请求,就执行该请求的 cancel 方法,同时从 requestList 中移除它;
- 把当前请求的 request 对象 push 到 requestList 中;
- 当接口响应之后,从 requestList 中移除。
下面是具体的代码部分:
//App.vue
export default {
data() {
return {
//当前正在执行的请求列表
requestList: [],
//可重复请求的白名单列表
sameRequestUrlWhiteList: []
};
},
created() {
//...其他操作
this.setAxiosInterceptors();
},
methods: {
setAxiosInterceptors() {
this.$http.interceptors.request.use(request => {
//给请求用单例模式配置一个独有的cancelToken
request.cancelToken = new axios.CancelToken(function executor(c) {
// 把cancel方法赋值到request的cancel属性上
request.cancel = c;
});
//判断当前请求是否在可重复请求的白名单中,如果不在,则进入
if (!this.sameRequestUrlWhiteList.includes(request.url)) {
//对requestList做过滤,从requestList中移除重复请求的同时,执行cancel方法
this.requestList = this.requestList.filter(item => {
if (
//通过url和method双重校验
item.url === request.url &&
item.method === request.method
) {
//取消请求,同时返回false给filter
item.cancel();
return false;
}
//不做操作,返回false给filter
return true;
});
}
//将当前请求push到requestList中
this.requestList.push(request);
//...其他处理
return request;
});
this.$http.interceptors.response.use(
response => {
//当接口响应之后,从 requestList 中移除
this.requestList = this.requestList.filter(item => {
if (
item.url === response.config.url &&
item.method === response.config.method
) {
return false;
}
return true;
});
//...其他处理
return response;
},
error => {
//错误处理
}
);
}
}
}
我们需要注意的是,取消接口会进入上面代码最后的那部分错误处理逻辑。正常业务中,我们都会对错误做一个统一的报错处理或者其他操作。这里可以在 catch 里通过 axios.isCancel 来判断是否被主动取消,来区别处理错误。
error => {
if (axios.isCancel(error)) {
//处理被取消的错误
} else {
//处理正常错误
this.$Message.error({
content: "请求失败,请联系管理员"
});
}
return Promise.reject(error.response);
}
以上就是通过拦截器和取消接口功能的结合运用,来解决相同接口重复请求的问题。
解决路由跳转时页面请求还未响应的问题
有了上面对重复请求的处理逻辑,这个问题已经很简单了。
很显然,我们只需要在路由跳转时,获取上面的 requestList,然后循环执行 cancel,最后将 requestList 清空。类似的,我们也需要定义一个白名单,在路由跳转时不需要 cancel。不多说,直接看代码,这里是在 Vue Router的导航守卫里添加了一个处理逻辑:
//router.js
//定义白名单
const cancelRequestUrlWhiteList = [];
//全局前置守卫
router.beforeEach(async (to, from, next) => {
//通过白名单过滤
requestList = requestList.filter(item => {
//如果不在白名单里,就cancel同时被过滤掉
if (!cancelRequestUrlWhiteList.includes(item.url)) {
item.cancel();
return false;
}
//如果在白名单里,就不会处理也不会被过滤
return true;
});
}
除了用导航守卫,也可以在具体某个组件卸载的生命周期函数里,定制化的做一些操作。
其他一些思考
上面提到的那个 requestList,始终存着当前整个应用所有正在执行中的请求,除了用来做取消操作,我们还可以把它维护在 Vuex 或者 Redux 中用,来响应式的做一些其他操作,例如:
- 通过给一个组件绑定一个属性 url,判断这个 url 在不在 requestList 中,来自动响应组件的loading状态或者disabled属性;
- 通过 requestList.length 来判断当前整个应用有没有正在执行请求,可以加一些特殊的动效,比如一个顶部加载进度条。
总结
关于 axios.CancelToken 在项目中的一些运用,就介绍到这里。