一、axios介绍
现在社区中有数量庞大的ajax(http)库,为何选择使用axios呢?
首先,因为它提供的API是Promise式的,目前业务代码基本都已经使用async/await来包裹异步api了。
那为何不使用基于fetch的类库呢?
因为,选用axios更重要的原因是,需要用到请求的abort。
abort
大部分场景中如果后端处理开销不大,前端使用类似Promise.race或标记位等方式都可以实现前端业务逻辑中的abort。但是如果该请求是一个非常重型的,对数据库读写有压力的请求时,一个实实在在的abort还是有必要的。
当然,可以在后端接口上,设计为创建任务、执行任务、取消任务这样的模式。
由于目前fetch没有abort方式(AbortController目前尚在实验阶段),所以只能使用XMLHttpRequest类来实现具备abort能力的ajax。
二、为何解读?
axios提供了cancel:
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
})
});
// cancel the request
cancel();
复制代码
实际业务代码示意:
axios({
method: 'get',
url: '***',
}).then(response => {
// 业务逻辑
}).catch(err => {
if (axios.isCancel(err)) {
// 取消请求
} else {
// 业务逻辑错误
}
})
复制代码
期望的结果是,当cancel后,会在业务代码的catch中捕获一个Cancel类型的错误。但实际使用中,该cancelError并没有触发,而是进入了response相关的业务逻辑。
于是,开始了一波debug。一开始怀疑是axios的坑,但当我打开github,看到该项目**4.8万+**的star数时,我确信:
一定是业务代码用错了!
三、代码
1. 文件结构
没有全部细看,把主流程的js看了一遍。
axios/lib
│
└───adpaters
│ │ ... ajax/http类的封装
│
└───cancel
│ │ ... 取消请求的相关代码
│
└───core
│ │
│ └───Axios.js 核心类,其余方法没细看
│
└───helpers
│ │ ... 工具函数集,没看
│
└───axios.js 入口文件,实例化了核心类
│
└───defaults.js 默认配置
复制代码
2. 主流程
请求发起
|
▼
+----------+
| req中间件 | axios称之为request interceptors
+----------+
|
▼
+----------+
| dispatch | 发起请求,内部包含了一些入参转化逻辑,不展开
+----------+
|
▼
+----------+
| Adapter | 适配器,根据环境决定使用http还是xhr模块
+----------+
|
▼
+----------+
| res中间件 | axios称之为response interceptors
+----------+
|
▼
+----------+
|transform | 返回值进行一次转换
+----------+
|
▼
请求结束
复制代码
3. 中间件
axios可以通过axios.interceptors来扩展request/response的中间件:
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Do something with response data
return response;
}, function (error) {
// Do something with response error
return Promise.reject(error);
});
复制代码
最后排查结果是某一个中间件出了问题导致的bug,下文再详细展开,先聚焦在中间件相关的源码上:
// core/Axios.js
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
复制代码
核心代码不长,它的目的是,转换出一个Promise数组:
[
ReqInterceptor_1_success, ReqInterceptor_1_error,
ReqInterceptor_2_success, ReqInterceptor_2_error,
...,
dispatchRequest, undefined,
ResInterceptor_1_success, ResInterceptor_1_error,
...,
]
复制代码
再将该数组转换为链式的Promise:
return Promise.resolve(
config,
).then(
ReqInterceptor_1_success, ReqInterceptor_1_error,
).then(
ReqInterceptor_2_success, ReqInterceptor_2_error,
).then(
dispatchRequest, undefined,
).then(
ResInterceptor_1_success, ResInterceptor_1_error,
)
复制代码
4. 请求取消
先贴一下主要源码:
// cancel/CancelToken.js
function CancelToken(executor) {
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
复制代码
这是CancelToken类的构造函数,它的入参需要是一个函数,该函数的第一个入参会返回cancel(message) => void
函数,该函数的作用是给CancelToken实例添加一个CancelError类型的reason属性。
axios有两个时机来取消请求。
第一种,在dispatchRequest方法中,在发起请求之前,如果cancel函数执行,throwIfCancellationRequested
会直接把cancelToken.reason抛出。
// core/dispatchRequest.js
function dispatchRequest(config) {
throwIfCancellationRequested(config);
// ...
}
复制代码
官网示例中的cancel示例就是这第一种取消方式。实际上,请求并没有在调用诸如axios.get方法时立刻发出,而是在microtask中执行(Event Loop相关文档可查阅此处)。具体源码参看上文中间件部分,即使没有任何request中间件,请求也是在Promise.resolve(config)
的后续中触发。
第二种,在请求发出以后,如果cancel函数执行,在实际的xhr模块中会触发abort。
// adapters/xhr.js
config.cancelToken.promise.then(function onCanceled(cancel) {
// 此处then会在CancelToken的resolvePromise执行后触发
request.abort();
reject(cancel);
});
复制代码
四、问题排查
1. 大致思路
确认源码以后,CancelError理论上都会被正确throw,并没有犯比较低级的return new Error('*')
问题。(可以想想为什么~)
既然如此,Error被抛出,那就一定是半路被捕获了。
那最有可能的原因是中间件出了问题,把CancelError给吞了。
2. 真相
最后确认,的确是有一个responseInterceptor:
axiosInstance.interceptors.response.use((resp: AxiosResponse) => {
//
}, (error: AxiosError): void => {
onResponseError(error);
});
// 而onResponseError是一个空方法
function onResponseError() {};
这会导致整个Promise链路变为:
Promise.resolve().then(() => {
return dispatch();
})
// response中间件
.then(data => {
return transform(data);
}, err => {
catchError(err); // 1. 没有继续抛出错误
}).then(data => {
// 2. 错误被中间件捕获后,进入后续resolved逻辑
}).catch(err => {
// 3. 无法捕获cancel错误
});
复制代码