1、现象
在开发过程中,系统在内网环境中,会调用一些只有现场环境才能访问的接口和一些第三方平台的接口。这些接口在内网环境下访问不了,在现场环境可以正常访问。这就导致在内网环境下,页面请求接口会导致请求超时,而且在请求过程中,突然关闭了当前页面,请求也会一直处于等待状态,直到超时。同时,这些接口的请求也会占用浏览器的资源,导致浏览器卡顿,影响整个页面的流畅度和展示效果。
在现场环境,一个接口由于数据量过大,页面请求很慢,关闭页面后,请求也会一直处于等待状态,其他页面也会受到影响。
2、解决方案
为了解决这个问题,针对内网环境,对相应的接口请求,通过专门的外部接口统计模块,制造一些静态的mock数据,这样就可以在本地开发环境中,模拟出相应的接口返回数据,而不用真实的数据。
同时,针对关闭当前页签,每次请求接口时,在请求拦截器中,存储当前页面的取消请求token,在页面关闭时,取消所有请求。这样就可以避免请求超时,也不会影响其他页面的展示,通过Axios的CancelToken机制,可以很好的解决这个问题。
3、Axios的CancelToken机制
3.1 创建CancelToken实例
创建一个CancelToken对象,并获取其中的token和cancel函数。这个token是一个用于标识请求的令牌,而cancel函数则用于触发请求的取消。
具体源码解析:
javascript
// lib/cancel/CancelToken.js
/**
* 首先定义了一个resolvePromise的静态变量,该静态变量是一个promise对象,在将来可以执行取消请求的操作
* 当这个 promise 的状态为 已成功(fulfilled),就会触发取消请求的操作 (执行then函数)
* 而执行resolve就能将promise的状态置为完成状态
*/
var resolvePromise;
// lib/cancel/CancelToken.js
/**
* 调用new Promise()生成一个promise实例,生成一个Promise实例赋值给 CancelToken 实例的promise属性,
* 将来定义then函数就是通过这个promise属性得到一个新的promise
*/
// 这里生成Promise实例时将 resolvePromise 福赋值给了CancelToken 实例的promise属性
this.promise = new Promise(function promiseExecutor(resolve) {
// 把 resolve 赋值给 resolvePromise,就是为了在这个 promise 外能执行 resolve 而改变这个promise的状态
resolvePromise = resolve;
});
// lib/cancel/CancelToken.js
/**
* 定义静态变量token,并将其赋值为CancelToken的实例,
* 后面执行取消操作时的onCanceled函数和由用户定义的取消请求的原因等信息都将添加到该静态变量上
*/
var token = this;
/**
* 在实例化CancelToken构造函数时会传入executor函数,执行该函数,将内部定义的cancel函数传递给executor,
* 即业务中调用的cancel就是CancelToken构造函数内部定义的cancel函数
*/
executor(function cancel(message) {
if (token.reason) {
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
// lib/adapters/xhr.js
// onCanceled是真正执行取消请求操作的函数
if (config.cancelToken || config.signal) {
onCanceled = function (cancel) {
if (!request) {
return;
}
reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
request.abort();
request = null;
};
// 订阅 onCanceled函数
// onCanceled 函数会被添加到CancelToken实例的 _listeners 数组中
// 当执行取消请求操作时,从_listeners中取出onCanceled函数,然后执行
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
// lib/cancel/CancelToken.js
/**
* 订阅onCanceled函数时,调用CancelToken实例的subscribe方法,将onCanceled添加到CancelToken实例的_listeners属性里,
* 当执行取消请求操作时,就从_listeners中取出onCanceled函数,执行该函数取消请求
*/
CancelToken.prototype.subscribe = function subscribe(listener) {
// 参数 listener 就是 onCanceled 函数
// this.reson 是取消请求的原因等信息。它是一个 Cancel实例对象
if (this.reason) {
listener(this.reason);
return;
}
// 将 onCanceled 添加到CancelToken实例的 _listeners 属性里
if (this._listeners) {
this._listeners.push(listener);
} else {
this._listeners = [listener];
}
};
流程图:

3.2 关联CancelToken
在发起请求时,将创建的CancelToken对象中的token作为请求配置的一部分,通过cancelToken参数传递给Axios。这样,Axios就知道在取消令牌触发时要取消哪个请求。
3.3 取消请求
当需要取消请求时,调用CancelToken对象中的cancel函数,并提供一个取消的原因(这个原因是可选的)。这会触发Axios内部的逻辑,导致底层的网络请求被中止。
具体源码解析:
javascript
// lib/cancel/CancelToken.js
/**
* 在执行cancel函数时,首先实例化一个Cancel实例对象,将用户传入的消息(取消请求的原因等信息)添加到Cancel实例对象中,
* 然后将这个Cancel实例对象添加得到cancelToken实例对象上,然后调用resolvePromise, 改变promise的状态
* 执行 executor 函数,将 cancel 方法传入 executor
* executor 是实例化 CancelToken 时传入的函数
* cancel 方法调用 resolvePromise,即触发取消请求的操作
* cancel 方法就是CancelToken实例化时函数参数 executor 的 参数 c /// new CancelToken(function executor(c) {}
*/
executor(function cancel(message) {
// token 是 CancelToken 实例
if (token.reason) {
return;
}
// 实例化Cancel,执行 cancel 时将用户传递的信息添加到 CancelToken 实例的 reason 属性上
token.reason = new Cancel(message);
// 这里执行的就是promise的resolve方法,改变状态
resolvePromise(token.reason);
});
// lib/cancel/CancelToken.js
/**
* 这里的resolvePromise就是初始化CancelToken实例的promise属性时对外暴露的resolve参数,
* 在外部执行resolve,将promise(CancelToken实例的promise)的状态转为fullfilled状态,然后进入promise的then回调中。
*/
resolvePromise(token.reason);
// lib/cancel/CancelToken.js
/**
* 这里的 cancel 参数是 Cancel 实例,即存储了用户取消请求时传入的信息
*/
this.promise.then(function (cancel) {
if (!token._listeners) return;
var i;
var l = token._listeners.length;
for (i = 0; i < l; i++) {
// 这里执行 onCanceled 函数
token._listeners[i](cancel);
}
// 重置 _listeners
token._listeners = null;
});
// lib/adapters/xhr.js
/**
* 执行onCanceled函数的时候,会执行 XMLHttpRequest 的 abort 方法取消请求,
* 也就是说,当用户调用cancel方法后,最终会执行abort方法取消请求,同时调用reject让外层的promise失败
*/
onCanceled = function(cancel) {
if (!request) {
return;
}
reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
// 调用xhr的abort()取消请求
request.abort();
request = null;
};
流程图:

3.4 捕获取消错误
如果请求在取消前已经发出,Axios会抛出一个名为Cancel的错误。可以使用axios.isCancel(error)来检查捕获到的错误是否是取消错误,并在.catch部分处理这个取消错误。
4、具体应用
4.1 定义取消请求token的store
javascript
import { Canceler } from 'axios'
import { defineStore } from 'pinia'
export const useCancelToken = defineStore('cancelToken', () => {
// 已当前页面为key,存储所有取消请求的token
const routeRequestMap = ref(new Map<string, Canceler[]>())
/**
* @name 存储当前页面的取消请求token
* @param route 当前页面的路由
* @param token 取消请求的token
*/
const addRouteCancelToken = (route: string, token: Canceler) => {
if (routeRequestMap.value.has(route)) {
routeRequestMap.value.get(route)?.push(token)
} else {
routeRequestMap.value.set(route, [token])
}
}
/**
* @name 清除当前页面的取消请求token
* @param route 当前页面的路由
*/
const clearRouteCancelToken = (route: string) => {
if (routeRequestMap.value.has(route)) {
const tokens = routeRequestMap.value.get(route)
tokens?.forEach((item) => {
item()
})
routeRequestMap.value.delete(route)
}
}
return {
addRouteCancelToken,
clearRouteCancelToken,
routeRequestMap
}
})
4.2 在请求拦截器中,存储当前页面的取消请求token
javascript
service.interceptors.request.use(
(config) => {
// 获取当前路由
const currentRoute = router.currentRoute.value
config.cancelToken = new axios.CancelToken((cancel: Canceler) => {
useCancelToken().addRouteCancelToken(currentRoute.path, cancel) // 存储cancel token到store中
})
}
)
4.3 在关闭当前页面时,取消当前页面的所有请求
javascript
const cancelToken = useCancelToken()
// 清除上次页面遗留的请求
cancelToken.clearRouteCancelToken(tag.path)
4.4 在请求响应拦截器中,处理取消请求的情况
javascript
service.interceptors.response.use(
(response) => {
return response
},
(error) => {
if (axios.isCancel(error)) {
// 取消请求的情况下,终端Promise调用链, 避免错误信息污染
console.log('请求取消')
return new Promise(() => {})
} else {
return Promise.reject(error)
}
}
)
5、总结
通过Axios的CancelToken机制,可以很好的解决在内网环境下,页面请求接口超时,浏览器卡顿,影响其他页面的展示的问题。通过存储当前页面的取消请求token,在页面关闭时,取消所有请求,避免请求超时,也不会影响其他页面的展示。