 Axios源码解读
Axios源码解读
  提示
本章节基于Axios 1.7.2版本撰写的源码解读,该版本是写该文章时Axios的最新版本
源码的所有注释已完成,点击这里查看:《Axios 1.7.2源码解读》 (opens new window)
逐行分析源码的文章正在撰写中~
解读文章正在撰写中~~
# Axios导出
Axios导出的内容列表如下:
- axios:默认导出,是一个Axios实例
- Axios:Axios类
- AxiosError:Axios错误类,用于创建Axios相关的错误对象
- CanceledError:请求取消错误类
- isCancel:用于判断某个对象是否是一个- CanceledError实例
- CancelToken:用于创建取消令牌的类
- VERSION:版本号
- all:用于发起多个axios并发请求
- Cancel:- CanceledError的别名,用于兼容
- isAxiosError:用于判断某个对象是否是一个- AxiosError(具有- isAxiosError:true)
- spread:- Function.prototype.apply的语法糖
- toFormData:将一个对象转化为- FormData
- AxiosHeaders:用于管理请求头的类
- HttpStatusCode:HTTP状态码列表
- formToJSON:将- FormData转为- Json格式
- getAdapter:获取有效的适配器的函数
- mergeConfig:用于合并两个axios配置的方法
默认导出的axios是由内部的createInstance函数调用创建的实例,实例上有一个create方法指向createInstance函数,可以用于创建自定义的axios实例。其余的导出都是实例上的方法。之所以导出一个axios的实例而不是直接导出Axios类作为默认导出,是为了使用的方便。一个原始的使用例子如下:
const axios = new Axios()
axios.request(url, config)
2
使用默认导出的axios实例,则可以这样使用
axios.get(url, config)
createInstance(defaultConfig)函数的基本逻辑如下:
function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig); // 创建Axios实例
  const instance = bind(Axios.prototype.request, context); // 创建request的绑定函数
  //  将Axios.prototype上的属性拷贝到instance上
  utils.extend(instance, Axios.prototype, context, { allOwnKeys: true });
  // 将Axios实例上的属性拷贝到instance上
  utils.extend(instance, context, null, { allOwnKeys: true });
  // 添加create方法,用于创建新的axios实例
  instance.create = function create(instanceConfig) {
    // 合并配置并创建实例
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };
  return instance;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
导出逻辑:
const axios = createInstance(defaults);
axios.Axios = Axios;// 在实力上暴露Axios类
// 接下来在axios上添加其它需要导出的属性和方法
// ...
axios.default = axios;
export default axios
2
3
4
5
6
7
可见导出的axios对象实际上Axios.prototype.request方法的绑定方法,该方法还拷贝了Axios.prototype以及Axios实例上的所有属性和方法,同时添加了其它需要导出的类和方法,如AxiosError,CancelToken等。
# axios调用
axios.post('/user', {
  firstName: 'Fred',
  lastName: 'Flintstone'
}, {
  headers: {
    'Content-Type': 'application/json'
  },
  transformRequest: [
    function(data, headers) {}
  ],
  transformResponse: [
    function(data) {}
  ],
  adapter: function(config) {
    return ['xhr']
  },
  //...
})
.then(function (response) {
  console.log(response);
})
.catch(function (error) {
  console.log(error);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
当我们按照类似如上方法调用axios发起一个请求时,axios内部做了以下的事情:
- 参数统一,Axios.prototype.request既可以接收一个url+config,也可以只接收一个包含url属性的config
- 合并选项:合并默认的选项以及传入的选项
- 获取请求方法等属性
- 解析请求头,生成最终用于创建发起请求的头部信息
- 初始化一个请求拦截器的链(数组)
- 设置请求拦截器,将所有请求拦截器逐个压入拦截器链的头部(倒序)
- 初始化一个响应拦截器的链(数组)
- 设置响应拦截器,将所有的响应拦截器逐个压入拦截器链的尾部
- 如果存在异步的请求拦截器
- 初始化一个请求链,并将dispatchRequest(用于发起请求的方法)压入链
- 将请求拦截器放到该链的前面,将响应拦截器放到该链的后面,这样就形成了一条:请求拦截器->请求->响应拦截器的链
- 使用Promise.resolve(config)创建一个初始的promise
- 遍历整个链,逐个调用promise的then方法,这样就形成了一条完整的promise链式调用
- 返回最终的promise
 
- 初始化一个请求链,并将
- 如果不存在异步的请求拦截器
- 创建一个新的配置对象newConfig
- 遍历请求拦截器,将其结果不断赋值给newConfig
- 这个过程如果有发生错误则会抛出错误停止执行
- 使用newConfig这个新的配置调用dispatchRequest函数发起请求,其返回值为一个promise
- 遍历响应拦截器,调用promise的then方法,形成一条新的promise链
- 返回promise
 
- 创建一个新的配置对象
# dispatchRequest(config)函数
 dispatchRequest函数是整个请求处理链条的核心,也是实际发起请求的地方。dispatchRequest函数接收一个config配置对象,返回一个promise,其调用后执行的过程如下:
- 检查请求是否已经取消(使用CancelToken或者new AbortController().signal),如果已经取消了,则会抛出中断的错误
- 实例化请求头为AxiosHeader实例
- 遍历请求转换器,传入相关配置调用请求转换器,生成转换后的请求数据
- 规范化请求头
- 根据配置获取请求适配器,目前支持['xhr', 'http', 'fetch'],也可以自定义适配器
- 使用获取到的适配器发起请求
- 请求成功
- 判断请求是否已经取消了,如果取消了,则会抛出中断错误
- 调用响应转换器,转换并生成最终响应的数据
- 响应头转为AxiosHeaders实例
- 返回响应结果
 
- 请求失败,接收失败对象reason- 如果reason不是CancelError- 判断请求是否已经取消了,如果取消了,则会抛出中断错误
- 如果有错误中包含reason.response属性(有响应,但是状态码超过了定义的成功范围),则调用响应转换器,转换并生成最终响应的数据,并将响应头转为AxiosHeaders实例
 
- 返回一个拒绝的Promise
 
- 如果
# 总结
Axios的核心其实是非常简单的,它接收配置后生成一个用于请求和处理请求数据、响应数据的链条,在发起请求成功或者失败后,将结果返回。Axios源码中比较复杂的部分是每一个适配器内部的参数处理,包括参数的序列化、规范化、流的处理等逻辑。尤其是FormData和Stream的处理。这部分的内容比较繁杂,详情查看源码注释。
另外,Axios新增的fetch适配器是有bug的,目前发现了一个处理跨域携带凭证信息时,处理withCredentials配置的bug。在fetch适配器中有如下代码:
// lib/adapters/fetch.js
if (!utils.isString(withCredentials)) {
  withCredentials = withCredentials ? 'cors' : 'omit';
}
request = new Request(url, {
  ...fetchOptions,
  signal: composedSignal,
  method: method.toUpperCase(),
  headers: headers.normalize().toJSON(),
  body: data, 
  duplex: "half",
  withCredentials
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
实际上,Request构造器是不支持withCredentials配置的,正确的配置是credentials,其有效值为omit、same-origin和include,并不包含cors。所以现有的现有适配器实现会导致跨域访问时无法设置携带Cookie。如下代码,withCredentials设置成true后,携带的Cookie仍然会是个空的对象。
axios.get('http://localhost:3000', {
  withCredentials: true,
  adapter: ['fetch']
})
.then(response => console.log(response.data))
  .catch(error => console.error('Error:', error));
2
3
4
5
6
修复这个bug只需要将源码修改如下:
if (!utils.isString(withCredentials)) {
  withCredentials = withCredentials ? 'include' : 'omit';
}
request = new Request(url, {
  ...fetchOptions,
  signal: composedSignal,
  method: method.toUpperCase(),
  headers: headers.normalize().toJSON(),
  body: data, 
  duplex: "half",
  credentials: withCredentials
});
2
3
4
5
6
7
8
9
10
11
12
13
14
我已经给axios提了issue:Fix fetch request config#6505 (opens new window)
