4

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

 3 years ago
source link: https://xieyufei.com/2020/07/18/Axios-One.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

  1. 从浏览器中创建 XMLHttpRequests
  2. 从 node.js 创建 http 请求
  3. 支持 Promise API
  4. 拦截请求和响应
  5. 转换请求数据和响应数据
  6. 自动转换 JSON 数据
  7. 客户端支持防御 XSRF

  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函数用来给某一函数指定调用时的上下文,其源码如下:

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的值
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是如何使用整合后的配置来通过适配器发起请求的。

更多前端资料请关注作者公众号``【前端壹读】``。
PS:公众号接入了图灵机器人小壹,欢迎各位老铁来撩。
follow.png

本文地址: http://xieyufei.com/2020/07/18/Axios-One.html

@谢小飞的网站

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK