深入学习Axios源码(构建配置)

  axios是我们日常代码中常用的一个http库,它可以用来在浏览器或者node.js中发起http请求;它强大的功能和简单易用的API受到了广大前端童鞋们的青睐;那么它内部是如何来实现的呢,让我们走进它的源码世界一探究竟。

  首先来看一下axios有哪些特性:

  1. 从浏览器中创建 XMLHttpRequests
  2. 从 node.js 创建 http 请求
  3. 支持 Promise API
  4. 拦截请求和响应
  5. 转换请求数据和响应数据
  6. 取消请求
  7. 谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  8. 自动转换 JSON 数据
  9. 客户端支持防御 XSRF
  10. 谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  axios的大致处理流程如下:

flow.jpg

多种请求方式

  axios除了以上的特性,还支持了多种请求方式,方便我们通过不同的方式来请求。

  第一种方式axios(option)

1
2
3
4
5
6
axios({
url,
method,
headers,
data
})

  第二种方式axios(url[, config])

1
2
3
4
5
axios('/api/list', {
method,
headers,
data
})

  第三种方式axios.request(url?,option),第三种方式同第一种方式本质上一样。

1
2
3
4
5
6
axios.request({
url,
method,
headers,
data
})

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

第四种方式axios.request(url,option),第四种方式同第三种方式本质上一样。

1
2
3
4
5
axios.request(url, {
method,
headers,
data
})

  第五种方式axios[method](url,option),这种请求方式主要针对get、delete、head、options方法。

1
2
3
4
axios.get(url,{
headers,
params
})

  第六种方式axios[method](url,data,option),这种请求方式主要针对post、put、patch方法。

1
2
3
axios.post(url, data, {
headers,
})

  这六种请求方式也是我们常见的方式;我们发现前两种请求方式axios作为一个函数直接来请求,而后面四种方式axios则是一个对象;因此我们猜测,axios首先肯定是一个函数,这个函数上又挂载了request、get、post等函数方便我们具体调用某个方法。

目录结构

  看代码前先来看一下项目的整体结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
├── /dist/                     # 项目输出目录
├── /lib/ # 项目源码目录
│ ├── /cancel/ # 定义取消功能
│ ├── /core/ # 一些核心功能
│ │ ├── Axios.js # axios的核心主类
│ │ ├── dispatchRequest.js # 用来调用http请求适配器方法发送请求
│ │ ├── InterceptorManager.js # 拦截器构造函数
│ │ └── settle.js # 根据http响应状态,改变Promise的状态
│ ├── /helpers/ # 一些辅助方法
│ ├── /adapters/ # 定义请求的适配器 xhr、http
│ │ ├── http.js # 实现http适配器
│ │ └── xhr.js # 实现xhr适配器
│ ├── axios.js # 对外暴露接口
│ ├── defaults.js # 默认配置
│ └── utils.js # 公用工具
├── package.json # 项目信息
├── index.d.ts # 配置TypeScript的声明文件
└── index.js # 入口文件

  可以发现,我们需要用到的代码大多在/lib目录下。

工具函数

  看源码之前我们首先来学习一下axios用到的几个易于混淆的工具函数,以及它们具体是用来实现什么功能的。

bind

  bind函数用来给某一函数指定调用时的上下文,其源码如下:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
8
9
10
//lib/helpers/bind.js
module.exports = function bind(fn, thisArg) {
return function wrap() {
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return fn.apply(thisArg, args);
};
};

  它的实现效果同Function.prototype.bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import bind from '/lib/helpers/bind.js'

var obj = {
name: 'hello'
}

function showName() {
console.log(this.name)
}

var instance = bind(showName, obj)
//效果同下
//var instance = showName.bind(obj)
instance()

forEach

  forEach用来遍历对象或者数组;我们知道对象需要for in遍历,而数组用for循环遍历,forEach将两者遍历的方式整合到一起,其源码如下:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//lib/utils.js
function forEach(obj, fn) {
if (obj === null || typeof obj === 'undefined') {
return;
}
//兼容不是数组对象的情况
//如果是基本数据类型同样放到数组中遍历
if (typeof obj !== 'object') {
obj = [obj];
}
if (isArray(obj)) {
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
}
}

  可以看出forEach对字符串或者数字等基本数据类型做了兼容,对数组和对象做了不同的遍历处理;其中如果遍历的是对象,回调函数每次返回对象的值、键以及对象本身。

1
2
3
4
5
6
import { forEach } from '/lib/utils.js'
forEach([1,2],(elem, index, array)=>{})
forEach({
name: 'hi',
age: 18
},(value, key, object)=>{})

extend

  extend将一个对象b上面所有的属性和方法扩展到另一个对象a上,并且指定方法调用的上下文,其源码如下:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
8
9
10
11
//lib/utils.js
function extend(a, b, thisArg) {
forEach(b, function assignValue(val, key) {
if (thisArg && typeof val === 'function') {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
});
return a;
}

  这里forEach就用来遍历对象了;a是目标对象,b是源对象,thisArg是执行上下文,我们可以通过代码尝试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { extend } from '/lib/utils.js'
var context = {
name: 'context'
}
var target = {
name: 'target',
say() {
console.log('i am target,my name is ' + this.name)
}
}
var source = {
name: 'source',
say() {
console.log('i am source,my name is ' + this.name)
}
}
extend(target, source, context)
//source
target.name
//i am source,my name is context
target.say()

  最后运行可以发现source对象上的属性方法都赋值到target对象上,执行上下文是context对象了。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

merge

  merge函数用来将多个对象深度合并为一个新的对象,其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//lib/utils.js
function merge(/* obj1, obj2, obj3, ... */) {
var result = {};
function assignValue(val, key) {
if (isPlainObject(result[key]) && isPlainObject(val)) {
result[key] = merge(result[key], val);
} else if (isPlainObject(val)) {
result[key] = merge({}, val);
} else if (isArray(val)) {
result[key] = val.slice();
} else {
result[key] = val;
}
}

for (var i = 0, l = arguments.length; i < l; i++) {
forEach(arguments[i], assignValue);
}
return result;
}

  isPlainObject判断一个对象是否是一个JS原生对象,即使用Object构造函数创建的对象;如果是对象的话就进行深度的合并,我们写一个demo测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { merge } from '/lib/utils.js'

var obj1 = {
a:1,
b: {
b1:1,
b2:2
}
}
var obj2 = {
a:2
b: {
b1:4,
b3:5
},
}
var newObj = merge(obj1, obj2)
//最后结果
//{
// a:2,
// b: {
// b1:4,
// b2:2,
// b3:5
// }
//}

构造实例

  介绍完了工具函数我们就真正的进入axios的核心源码部分;首先在index.js中,我们看到通过module.exports = require('./lib/axios');导出了axios,因此我们找到/lib/axios文件:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//省略部分代码
///lib/axios.js
var Axios = require('./core/Axios');
var utils = require('./utils');
var bind = require('./helpers/bind');
//默认配置
var defaults = require('./defaults');

function createInstance(defaultConfig) {
//创建axios实例
var context = new Axios(defaultConfig);
//将instance指向Axios.prototype.request函数
var instance = bind(Axios.prototype.request, context);
//将Axios.prototype上的属性和方法扩展到instance上
utils.extend(instance, Axios.prototype, context);
//将context上的属性和方法扩展到instance上
utils.extend(instance, context);
return instance;
}

var axios = createInstance(defaults);
module.exports = axios;

  这段代码看上去比较绕,不过我们发现核心部分就是通过createInstance创建了一个axios实例对象,创建的同时传入了defaultConfig对象(根据名字我们也能猜出来这是默认配置),然后将实例对象导出;因此createInstance创建的就是我们使用的那个axios函数。

  由于createInstance创建是通过Axios构造函数创建的,因此我们把createInstance放一放,先看一下Axios构造函数做了哪些操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var utils = require('./../utils');
var buildURL = require('../helpers/buildURL');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');
function Axios(instanceConfig) {
//默认配置
this.defaults = instanceConfig;
//拦截器
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}

Axios.prototype.request = function request(config) {
//兼容axios.request(url,config)的情况
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
config = mergeConfig(this.defaults, config);
var promise = Promise.resolve(config);
//省略部分发送请求的代码
return promise;
};

utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url
}));
};
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
Axios.prototype[method] = function(url, data, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: data
}));
};
});
module.exports = Axios;

  Axios构造函数仅仅做了两个事,一个是将默认配置保存到defaults,另一个则是构造了interceptors拦截器对象;Axios函数在原型对象上还挂载了request、get、post等函数,但是get、post等函数最终都是通过request函数来发起请求的。而且request函数最终返回了一个Promise对象, 因此我们才能通过then函数接收到请求结果。对原型链不了解的童鞋可以看这篇文章一文读懂JS中类、原型和继承

  了解了Axios构造函数的本质,让我们再回到createInstance函数:

1
2
3
4
5
6
7
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
utils.extend(instance, Axios.prototype, context);
utils.extend(instance, context);
return instance;
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  我们发现Axios构造出了实例对象context,然而createInstance并不是直接返回了context对象;这是因为上面我们也说了axios是一个函数,然而context是对象,返回对象的话是并不能直接调用的,那怎么办呢?

how.jpg

  我们在Axios源码中发现,真正调用的是Axios原型链上的request方法;因此导出的axios需要关联到request方法,这里巧妙的通过bind函数进行关联,生成关联后的instance函数,同时指定它的调用上下文就是Axios的实例对象,因此instance调用时也能获取到实例对象上的defaults和interceptors属性;但是仅仅关联request还不够,再通过extend函数将Axios原型对象上的所有get、post等函数扩展到instance函数上,因此这也是我们才能够使用多种方式调用的原因所在。

  同时,如果我们需要创建多个axios实例,但是某几个axios实例的配置(用了同样的域名等)是一样的,我们不希望每次都要写重复的配置,axios还提供了另一种创建实例模板的方式:

1
2
3
4
5
6
7
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
instance.get('/list')
instance.post('/add')

  通过create函数创建了一个有默认配置的实例,这样我们只需要愉快的调用axios的API方法即可;需要请求其他域名只需要再次create即可,这也是工厂模式的一种体现,它的源码实现也很简单,也是通过createInstance创建一个合并配置后的实例:

1
2
3
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

配置合并

  实例对象创建好之后,我们就需要把配置进行合并,方便后面发送请求。在看源码前,我们发现上面代码中主要有两种config,一种是defaultConfig,即默认配置,在构造实例的时候传入;另一种是userConfig,也就是我们调用实例进行请求时传入的配置;在上面的代码中,也出现了很多次mergeConfig这个函数,根据命名我们也能看出来,这是用来合并两种配置的。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

默认配置

  首先让我们看一下axios给我们默认配置了哪些属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//lib/defaults
var utils = require('./utils');
var DEFAULT_CONTENT_TYPE = {
'Content-Type': 'application/x-www-form-urlencoded'
};
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
//浏览器环境使用xhr
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined'
&& Object.prototype.toString.call(process) === '[object process]') {
//node环境使用http
adapter = require('./adapters/http');
}
return adapter;
}
var defaults = {
adapter: getDefaultAdapter(),
transformRequest: [function transformRequest(data, headers) {
//省略转换请求数据代码
return data;
}],
transformResponse: [function transformResponse(data) {
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) { /* Ignore */ }
}
return data;
}],
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
}
};
defaults.headers = {
common: {
'Accept': 'application/json, text/plain, */*'
}
};
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
defaults.headers[method] = {};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});
module.exports = defaults;

  可以发现,axios定义了默认的适配器(用于发送请求)、转换器(转换请求和响应数据)和请求头等一些数据;getDefaultAdapter函数用来获取默认的适配器,这样在浏览器端和node环境都可以发送请求;可以看到axios给每个请求方法都定义了一个默认的请求头和一个公共的请求头common,在后面发送请求时会根据传入的请求方法类型使用相应的请求头。

属性分类

  下面我们就来看一下mergeConfig是如何将两种config合并的;首先mergeConfig将所有config中用到的字段进行了划分,分成了三类:

  1. 没有初始值,其值必须由初始化的时候指定
  2. 需要合并的属性,这些属性一般都是对象,处理时需要将对象进行合并
  3. 普通属性,这些属性一般都是值类型,如果userConfig中有,则以userConfig为准;没有取defaultConfig的值
  4. 谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//可以把config1看做defaultConfig,config2看做userConfig
module.exports = function mergeConfig(config1, config2) {
config2 = config2 || {};
//最终返回合并后的配置
var config = {};
//第一种属性
var valueFromConfig2Keys = ['url', 'method', 'data'];
//第二种属性
var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params'];
//第三种属性
var defaultToConfig2Keys = [
'baseURL', 'transformRequest', 'transformResponse', 'paramsSerializer',
'timeout', 'timeoutMessage', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName',
'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'decompress',
'maxContentLength', 'maxBodyLength', 'maxRedirects', 'transport', 'httpAgent',
'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding'
];
var directMergeKeys = ['validateStatus'];
//省略下面代码
}

  对于第一种属性,url、method和data不能从默认配置中取值,因此如果userConfig中有就直接取userConfig中的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getMergedValue(target, source) {
if (utils.isPlainObject(target) && utils.isPlainObject(source)) {
return utils.merge(target, source);
} else if (utils.isPlainObject(source)) {
return utils.merge({}, source);
} else if (utils.isArray(source)) {
return source.slice();
}
return source;
}
utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) {
if (!utils.isUndefined(config2[prop])) {
config[prop] = getMergedValue(undefined, config2[prop]);
}
});

  对于第二种属性,如果userConfig中有,就将其与defaultConfig进行合并。

1
2
3
4
5
6
7
8
function mergeDeepProperties(prop) {
if (!utils.isUndefined(config2[prop])) {
config[prop] = getMergedValue(config1[prop], config2[prop]);
} else if (!utils.isUndefined(config1[prop])) {
config[prop] = getMergedValue(undefined, config1[prop]);
}
}
utils.forEach(mergeDeepPropertiesKeys, mergeDeepProperties);

  对于第三种普通属性,如果userConfig中有就取userConfig,没有就取defaultConfig中的值。

1
2
3
4
5
6
7
utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) {
if (!utils.isUndefined(config2[prop])) {
config[prop] = getMergedValue(undefined, config2[prop]);
} else if (!utils.isUndefined(config1[prop])) {
config[prop] = getMergedValue(undefined, config1[prop]);
}
});

  对于除这三种属性之外的其他属性,当做第二种属性处理,通过mergeDeepProperties函数进行整合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var axiosKeys = valueFromConfig2Keys
.concat(mergeDeepPropertiesKeys)
.concat(defaultToConfig2Keys)
.concat(directMergeKeys);

//获取不在上述三种属性的中的其他属性的数组
var otherKeys = Object
.keys(config1)
.concat(Object.keys(config2))
.filter(function filterAxiosKeys(key) {
return axiosKeys.indexOf(key) === -1;
});

utils.forEach(otherKeys, mergeDeepProperties);

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

总结

  axios的源码还是有很多的地方值得我们来深入学习的,比如工具函数和它如何构造实例等;我们根据axios的多种请求方式,找到了它在构造实例时巧妙的绑定方式来实现多种请求的调用,构造实例后就需要将用户传入的配置和默认的配置进行整合起来;在下一篇文章中我们会了解axios是如何使用整合后的配置来通过适配器发起请求的。


本网所有内容文字和图片,版权均属谢小飞所有,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表。如需转载请关注公众号【前端壹读】后回复【转载】。