记一次axios源码排查

3,216 阅读4分钟

一、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错误
});