axios源码学习(一):通过axios有多少种方法可以发起请求?

花了两天时间读了一遍axios源码,终于理解了axios内部是怎么运行的,这期间学习到了很多,所以想分享一下。想了很久,决定用一些问题来作为引子,一点点的整理一下我对axios源码的理解。下面我会用到一些官方文档的用例来说明。

用过axios的童鞋应该都知道,axios有很多请求的api,用起来是非常方便的。那么大家知道axios有多少种方法方式可以发起请求吗?为什么这些方法方式都可以发起请求?内部是怎么实现的。

这里我会先整理一下有哪些方法api,然后最后来整理一下源码中是如果实现这些方法的。

axios发起请求的方法

我们现在说说有哪些方法。首先要说明一下,我们一般都是这样引入axios的:

1
import axios from 'axios'

axios()

axios()是可以直接作为函数调用的。本身就是一个请求函数,可以直接传入请求的url,请求数据及请求配置。

1
2
3
4
5
6
7
8
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});

其实内部相当于调用了createInstance(config)方法。

axios.create()

这个方法不是能直接发起请求的,而是创建一个Axios实例,然后在通过实例的request()方法或者其他实例上的方法去发起请求。

1
2
3
4
5
6
7
// Create an instance using the config defaults provided by the library
// At this point the timeout config value is `0` as is the default for the library
const instance = axios.create();

instance.get('/longRequest', {
timeout: 5000
});

axios[method]()

这个方法比较直观,根据请求的method,可以选择对应的请求方法,比如get请求可以使用axios.get,post请求可以使用axios.post

1
2
3
4
5
6
7
8
axios.get('/user/12345')
.then(function (response) {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
});

这些方法其实都是封装了axios.request()方法的,只是把传入的参数中method固定下来。

new axios.Axios

这个也是用来创建axios实例的,然后再用axios实例去调方法实现调起请求。

axios.request()

request()是axios中最重要的方法之一,上面所有的调起请求的方式都是基于这个方法,也是我们下面学习源码最先解读的内容。

内部实现

关键函数request()的实现

其实request()方法的源码非常少,很容易理解。主要分成两个部分,第一部分是整理合并config,第二部分是将拦截器的函数入栈chain,然后用Promise来执行。

这两个部分虽然没有提到发请求,但显而易见的是用Promise来执行时肯定是发了请求的。我们一步步来解读。

首先,第一部分是整理合并config。在这里,请求的配置除了传参进来的config之外,还有在axios.defaults中的配置。所以,需要先拿到config,然后跟axios.defaults得到本次请求的配置。

request()方法的源码中,对config做了统一格式的处理。因为我们知道request()方法的传参其实支持不止一个。

1
2
3
4
5
6
7
8
9
10
11
12
// 第一种传参方式
axios.request({
url: '/api',
method: 'get'
// ...
})

// 第二种传参方式
axios.request('/api', {
method: 'get'
// ...
})

所以会先判断config是否是String类型,如果是,则判断为传入url,第二个传参才是config,否则第一个传参就是config。同时也要判断没有传config的可能

1
2
3
4
5
6
7
// axios.request 源码
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}

然后就可以合并请求配置了。源码中使用了mergeConfig()这个方法来处理两个配置对象的合并。这个方法内部其实是针对axios的所有配置做了专门的处理。

1
config = mergeConfig(this.defaults, config);

合并配置方法mergeConfig()

这个方法传参只有两个,config1config2。内部处理的流程是声明了三类配置属性的数组valueFromConfig2Keys, mergeDeepPropertiesKeys, defaultToConfig2Keys,然后针对这三类做针对性的浅拷贝,深拷贝,默认值处理,接着再处理除这三类配置属性外其他配置属性的赋值,最后返回最终完整配置对象。

从整个处理流程来说,我认为方法中主要处理的三类配置属性应该是比较主要的配置,而且因为各自配置的不同需要分别处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// axios mergeConfig 源码
/**
* valueFromConfig2Keys这个数组保存有需要从用户获得的配置项
* mergeDeepPropertiesKeys这个数组保存有需要深拷贝的配置项
* defaultToConfig2Keys这个数组保存有axios已有默认值的配置项
*/
var valueFromConfig2Keys = ['url', 'method', 'data'];
var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params'];
var defaultToConfig2Keys = [
'baseURL', 'url', 'transformRequest', 'transformResponse', 'paramsSerializer',
'timeout', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName',
'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress',
'maxContentLength', 'maxBodyLength', 'validateStatus', 'maxRedirects', 'httpAgent',
'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding'
];

valueFromConfig2Keys这类比较简单,但需要用户提供的,直接赋值即可:

1
2
3
4
5
6
7
// axios mergeConfig 源码
// 利用utils.forEach处理需要从用户获得的配置项
utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) {
if (typeof config2[prop] !== 'undefined') {
config[prop] = config2[prop];
}
});

mergeDeepPropertiesKeys这类配置的值类型可能是对象,需要做深拷贝:

1
2
3
4
5
6
7
8
9
10
11
12
13
// axios mergeConfig 源码
// 利用utils.forEach处理可能需要深拷贝的配置项
utils.forEach(mergeDeepPropertiesKeys, function mergeDeepProperties(prop) {
if (utils.isObject(config2[prop])) { // 如果config2配置项值为对象,则使用深拷贝赋值
config[prop] = utils.deepMerge(config1[prop], config2[prop]);
} else if (typeof config2[prop] !== 'undefined') { // 如果 config2配置项不为undefined,则直接赋值
config[prop] = config2[prop];
} else if (utils.isObject(config1[prop])) { // 以上判断完config2,就判断config1的配置值是否为Object,是着使用深拷贝赋值
config[prop] = utils.deepMerge(config1[prop]);
} else if (typeof config1[prop] !== 'undefined') { // 否则判断不为undefined就直接赋值
config[prop] = config1[prop];
}
});

defaultToConfig2Keys这类配置是已有默认配置,所以可以使用默认值赋值。但内部还是做了undefined的判断,因为axios.default的配置值用户是可以修改的:

1
2
3
4
5
6
7
8
9
// axios mergeConfig 源码
// 利用utils.forEach来处理其他axios已有默认值的配置项
utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) {
if (typeof config2[prop] !== 'undefined') { // 如果config2中有配置且不为undefined,则赋值
config[prop] = config2[prop];
} else if (typeof config1[prop] !== 'undefined') { // 否则判断config1中配置不为undefined,则赋值
config[prop] = config1[prop];
}
});

最后的部分就是集合三个分类的配置属性,去筛选出不在这其中的用户配置属性,再去处理赋值,只做浅拷贝。这些配置可能用户也会在axios.default上设置默认值,所以也要考虑处理默认值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// axios mergeConfig 源码
// 合并所有前面已处理赋值的配置属性
var axiosKeys = valueFromConfig2Keys
.concat(mergeDeepPropertiesKeys)
.concat(defaultToConfig2Keys);

// 筛选出前面未处理到的用户配置属性
var otherKeys = Object
.keys(config2)
.filter(function filterAxiosKeys(key) {
return axiosKeys.indexOf(key) === -1;
});

// 处理前面未处理到的用户配置属性赋值
utils.forEach(otherKeys, function otherKeysDefaultToConfig2(prop) {
if (typeof config2[prop] !== 'undefined') { // 判断config2中配置非undefined则赋值
config[prop] = config2[prop];
} else if (typeof config1[prop] !== 'undefined') { // 否则再判断config1中是否有默认非undefined值,有则赋值
config[prop] = config1[prop];
}
});

处理拦截器及请求

在这之前,先处理了请求的method,如果用户有传就将值改全小写,都没有就默认为get

接下来的部分我认为很精巧。在调请求之前,因为axios还支持请求拦截器跟响应拦截器,所以这里使用类栈的方案。

先设置一个数组,这个数组默认有两个元素,一个是dispatchRequest,就是发送请求的方法,另一个是undefined。然后遍历请求拦截器,把其中的两个函数从数组的头部插入,再遍历响应拦截器,把两个函数从数组的尾部插入。这样一来,数组的结构就是从头到尾为就是按照请求的过程排好了请求拦截器一对对函数,然后是请求函数跟undefined,然后是响应拦截器一对对函数。

然后声明一个Promise.resolve()后返回的promise, 把数组中从头部开始一对对函数的传入promise.then()中。

这个操作可以利用promise.then()的特点,自动通过promise链去调用方法。如果请求拦截器中有报错,则最后不会调dispatchRequest方法并断掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// axios.request 源码
// Hook up interceptors middleware
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使得我们调请求时可以用promise的方法继续处理。

其他请求方法的实现

那么request()这个方法是怎么演变出这么多种调用方式的呢?

首先,在Axios这个类下,提供了一堆axios[method]()的方法,作者是通过两个forEach来实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Axios 源码
// Provide aliases for supported request methods
// 这里提供了methods方法,是通过封装request来实现的,换言之所有逻辑都在request中
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, data, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data
}));
};
});

然后引入的axios,实际是先调用了createInstance()方法,这个方法里声明一个instance的变量,通过bind方法得到了一个上下文为Axios实例的Axios.prototype.request()方法,最后返回instance。所以我们可以直接调用axios方法发请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// axios 源码
function createInstance(defaultConfig) {
// 声明一个Axios实例
var context = new Axios(defaultConfig);
// 返回一个function,this指向context,即Axios实例
// 相当于instance(args)会变成Axios.prototype.request.apply(context, args)
var instance = bind(Axios.prototype.request, context);

// Copy axios.prototype to instance
// 把Axios.prototype上的属性方法复制到instance上,this指向context
utils.extend(instance, Axios.prototype, context);

// Copy context to instance
// 把context复制到instance
utils.extend(instance, context);

// 返回的instance上有Axios类上的方法,有Axios实例上所有属性跟方法
return instance;
}

// Create the default instance to be exported
var axios = createInstance(defaults);

axios.create()方法内部就是直接使用了createInstance(), axios.Axios就是直接暴露Axios类。

结尾

关于axios请求的源码分析就整理完了,下一篇会来了解一下axios是怎么实现请求取消的。