0%

Axios源码解析(五):核心工具方法(2)

上篇 Axios 源码解析(四):核心工具方法(1) 解析了 default.js/cancel 目录下的源码,下面继续解析 /cancel 目录下核心工具方法的代码。

https://github.com/MageeLin/axios-source-code-analysis 中的 analysis 分支可以看到当前已解析完的文件。

总览

《Axios 源码解析(一):模块分解》中已经分析过,/core 目录中包含如下这些文件:

1
2
3
4
5
6
7
8
9
10
11
├─core
│ Axios.js
│ buildFullPath.js
│ createError.js
│ dispatchRequest.js
│ enhanceError.js
│ InterceptorManager.js
│ mergeConfig.js
│ README.md
│ settle.js
│ transformData.js

同样在 README.md 中,介绍了该目录的作用:

core/ 中的模块是特定于 axios 内部使用的模块。因为它们的逻辑特定于专门情况,所以在 axios 模块之外使用这些模块很可能没有用。core 模块的一些例子如下:

  • 调度请求
    • 通过 adapters/ 发送的请求(参见 lib/adapters/README.md
  • 管理拦截器
  • 处理配置

整个 core/ 目录中,Axios.js 是最复杂也是最核心的,放在最后,剩余的文件咱们挨个来看:

buildFullPath.js

这个 buildFullPath 方法组合了上篇分析过的 isAbsoluteURLcombineURLs 方法,最终目的让返回的 url 必须为一个绝对地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var isAbsoluteURL = require('../helpers/isAbsoluteURL');
var combineURLs = require('../helpers/combineURLs');

/**
* 仅当请求的 URL 不是绝对 URL 时,才通过将 baseURL 与请求的 URL 组合来创建新 URL。
* 如果请求的 URL 是绝对URL,此函数会原封不动地返回所请求的 URL。
*
* @param {string} baseURL base URL
* @param {string} requestedURL 要组合的url(绝对或相对)
* @returns {string} 组合后的url
*/
module.exports = function buildFullPath(baseURL, requestedURL) {
// 仅当请求的 URL 不是绝对 URL 时,才组合
if (baseURL && !isAbsoluteURL(requestedURL)) {
return combineURLs(baseURL, requestedURL);
}
// 否则直接返回
return requestedURL;
};

enhanceError.js

enhanceError 方法就是单纯为了升级 Error 对象,给 Axios 生成的错误对象添加 configcoderequestresponse 属性

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
/**
* 使用指定的配置、错误代码和响应来升级Error。
*
* @param {Error} error 要升级的error对象
* @param {Object} config 配置.
* @param {string} [code] 错误代码 (比如, 'ECONNABORTED').
* @param {Object} [request] 请求对象.
* @param {Object} [response] 响应对象.
* @returns {Error} 返回升级后的error对象.
*/
module.exports = function enhanceError(error, config, code, request, response) {
// 把各种属性赋给error对象
error.config = config;
if (code) {
error.code = code;
}

error.request = request;
error.response = response;
error.isAxiosError = true;

// 自定义一个toJSON方法,进行了序列化
error.toJSON = function toJSON() {
return {
// Standard
message: this.message,
name: this.name,
// Microsoft
description: this.description,
number: this.number,
// Mozilla
fileName: this.fileName,
lineNumber: this.lineNumber,
columnNumber: this.columnNumber,
stack: this.stack,
// Axios
config: this.config,
code: this.code,
};
};
return error;
};

createError.js

enhanceError 方法增强的就是 createError 方法生成的对象,Error 对象刚生成的时候只有一个 message 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var enhanceError = require('./enhanceError');

/**
* 使用指定的message、配置、错误代码、请求和响应来创建Error。
*
* @param {string} message 错误 message.
* @param {Object} config 配置.
* @param {string} [code] 错误代码 (比如 'ECONNABORTED').
* @param {Object} [request] 请求对象.
* @param {Object} [response] 响应对象.
* @returns {Error} 创建后的错误对象.
*/
module.exports = function createError(
message,
config,
code,
request,
response
) {
// 创建一个错误后给它添加各种属性
var error = new Error(message);
return enhanceError(error, config, code, request, response);
};

settle.js

settle 方法是用来 settle 请求过程这个 promise 的,结果是 resolve 还是 reject 取决于 response

如果响应成功,则 resolve(response);如果响应失败,则用 createError 生成一个错误对象,然后 reject 掉。

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
var createError = require('./createError');

/**
* 依据响应状态来 resolve 或 reject 一个 Promise.
*
* @param {Function} resolve resolve函数.
* @param {Function} reject reject函数.
* @param {object} response 响应对象.
*/
module.exports = function settle(resolve, reject, response) {
var validateStatus = response.config.validateStatus;
// 只有validateStatus方法校验通过时,才会resolve
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response);
// 否则reject升级后的错误对象
} else {
reject(
createError(
'Request failed with status code ' + response.status,
response.config,
null,
response.request,
response
)
);
}
};

mergeConfig.js

由于 Axios 中有默认配置、用户默认配置和用户自定义配置多种 config,所以 mergeConfig 就是用专门的规则来合并这些配置的,会返回一个新的合并后配置对象。

merge 的规则比较复杂,针对不同的属性需要有不同的合并策略:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
'use strict';

var utils = require('../utils');

/**
* 配置专用的的合并函数,通过将两个配置对象合并在一起,创建一个新的配置对象。
*
* @param {Object} config1
* @param {Object} config2
* @returns {Object} 从config1和config2合并出来的新对象
*/
module.exports = function mergeConfig(config1, config2) {
// 给config2默认为空对象
// eslint-disable-next-line no-param-reassign
config2 = config2 || {};
var config = {};

// 全部来源于config2的键
var valueFromConfig2Keys = ['url', 'method', 'data'];
// 优先来源于config2的键,深度合并会判别undefined
var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params'];
// 优先来源于config2的键,不进行深度合并
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',
];
// 优先来源于config2的键,直接深度合并 使用in来判断
var directMergeKeys = ['validateStatus'];

/**
* @description: 两个同名属性 merge 时的规则,source 比 target 优先级高
* @param {Object} target 要合并的属性
* @param {Object} source 源属性
* @return {Object} 最后返回source
*/
function getMergedValue(target, source) {
// 普通对象直接执行merge方法
if (utils.isPlainObject(target) && utils.isPlainObject(source)) {
return utils.merge(target, source);
// 如果target不是纯对象,就把target抛弃
} else if (utils.isPlainObject(source)) {
return utils.merge({}, source);
// 如果source是个数组,就返回source的副本
} else if (utils.isArray(source)) {
return source.slice();
}
return source;
}

/**
* @description: 深度合并方法
* @param {string} prop 键
*/
function mergeDeepProperties(prop) {
// 如果config2中该键存在,就直接执行合并
if (!utils.isUndefined(config2[prop])) {
config[prop] = getMergedValue(config1[prop], config2[prop]);
// 如果config2中该键不存在,但config1中该键存在,就直接取config1
} else if (!utils.isUndefined(config1[prop])) {
config[prop] = getMergedValue(undefined, config1[prop]);
}
}

// 1.如果是需要config2的键,就直接从config2中把值拿过来,绝对不会取config1
utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) {
if (!utils.isUndefined(config2[prop])) {
config[prop] = getMergedValue(undefined, config2[prop]);
}
});

// 2.需要深度合并的键就执行深度合并方法
utils.forEach(mergeDeepPropertiesKeys, mergeDeepProperties);

// 3.config2优先的键,就优先取config2,如果没有才取config1
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]);
}
});

// 4.直接合并,这个跟深度合并的区别是用in来做判别,也就是undefined的区别
utils.forEach(directMergeKeys, function merge(prop) {
if (prop in config2) {
config[prop] = getMergedValue(config1[prop], config2[prop]);
} else if (prop in config1) {
config[prop] = getMergedValue(undefined, config1[prop]);
}
});

// axios专用的配置的键
var axiosKeys = valueFromConfig2Keys
.concat(mergeDeepPropertiesKeys)
.concat(defaultToConfig2Keys)
.concat(directMergeKeys);

// 用户加的自定义配置的键
var otherKeys = Object.keys(config1)
.concat(Object.keys(config2))
.filter(function filterAxiosKeys(key) {
// 把axios专用的键过滤掉
return axiosKeys.indexOf(key) === -1;
});

// 5.用户自定义配置的键执行深度合并
utils.forEach(otherKeys, mergeDeepProperties);

return config;
};

transformData.js

transformData 方法是用来支持 transformRequesttransformResponse 的,transformRequest 允许在向服务器发送请求前修改请求数据,transformResponse 允许在传递结果给 then/catch 前修改响应数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var utils = require('./../utils');
var defaults = require('./../defaults');

/**
* 转换request或者response对象
*
* @param {Object|String} data 要被转换的对象
* @param {Array} headers 请求或响应的header
* @param {Array|Function} fns 单个函数或者函数数组
* @returns {*} 转换后的对象
*/
module.exports = function transformData(data, headers, fns) {
var context = this || defaults;
/*eslint no-param-reassign:0*/
utils.forEach(fns, function transform(fn) {
// 主要是调用了fn方法来转换
data = fn.call(context, data, headers);
});

return data;
};

dispatchRequest.js

dispatchRequest 方法最直接的执行发送请求的方法,它分为如下几步:

  1. 根据配置转换请求对象
  2. 整理请求头
  3. 选择一个适配器并发送请求
  4. 根据配置转换响应对象
  5. 返回响应
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');

/**
* 如果取消请求的行为已经执行,则抛出一个 Cancel 对象
*/
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}

/**
* 使用已经配置好的适配器,向服务器发送请求
*
* @param {object} config 被用于发送请求的配置
* @returns {Promise} 处理完的promise
*/
module.exports = function dispatchRequest(config) {
// 若请求已取消,则抛出Cancel对象
throwIfCancellationRequested(config);

// 确保header存在
config.headers = config.headers || {};

// 利用header和data,以及请求转换器来转换data
config.data = transformData.call(
config,
config.data,
config.headers,
config.transformRequest
);

// 合并请求头
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers
);

// 清理掉headers中的请求method
utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);

// 选择一个适配器
var adapter = config.adapter || defaults.adapter;

// 返回一个promise
return adapter(config).then(
function onAdapterResolution(response) {
// 同样,若请求已取消,则抛出Cancel对象
throwIfCancellationRequested(config);

// 利用header和data,以及响应转换器来转换data
response.data = transformData.call(
config,
response.data,
response.headers,
config.transformResponse
);

return response;
},
function onAdapterRejection(reason) {
// 同样,若请求已取消,则抛出Cancel对象
if (!isCancel(reason)) {
throwIfCancellationRequested(config);

// 利用header和data,以及响应转换器来转换data
if (reason && reason.response) {
// 添加到reason下的response属性中
reason.response.data = transformData.call(
config,
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}

return Promise.reject(reason);
}
);
};

InterceptorManager.js

InterceptorManager 是拦截器的类,使用栈保存了自定义的拦截器,use 方法安装拦截器,eject 方法卸载拦截器,forEach 迭代每一个拦截器。

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
var utils = require('./../utils');

function InterceptorManager() {
this.handlers = [];
}

/**
* 给栈中添加一个新的拦截器
*
* @param {Function} fulfilled fulfilled后怎么处理then的函数
* @param {Function} rejected reject后怎么处理reject的函数
*
* @return {Number} 便于之后删除拦截器用的ID
*/
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected,
synchronous: options ? options.synchronous : false,
runWhen: options ? options.runWhen : null,
});
return this.handlers.length - 1;
};

/**
* 从栈中删除一个拦截器
*
* @param {Number} id 使用use方法给的id来删除
*/
InterceptorManager.prototype.eject = function eject(id) {
// 直接把对应的地方设为null
if (this.handlers[id]) {
this.handlers[id] = null;
}
};

/**
* 迭代已注册的拦截器
*
* 该方法对于跳过任何可能变成"null"的拦截器。
*
* @param {Function} fn 使用fn方法来调用每一个拦截器
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};

module.exports = InterceptorManager;

Axios.js

Axios.js/core 目录下的最终出口,也是生成 Axios 类的位置。Axios 的构造函数结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Axios(instanceConfig) {
this.defaults;
this.interceptors;
}
Axios.prototype.request;
Axios.prototype.getUri;
Axios.prototype.delete;
Axios.prototype.get;
Axios.prototype.head;
Axios.prototype.options;
Axios.prototype.post;
Axios.prototype.put;
Axios.prototype.patch;

其中最核心的方法也就是 Axios.prototype.request 方法,在该方法中主要执行三步:

  1. 处理请求拦截器
  2. 发送请求
  3. 处理响应拦截器
  4. 返回响应

源码如下:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
var utils = require('./../utils');
var buildURL = require('../helpers/buildURL');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');
var validator = require('../helpers/validator');

var validators = validator.validators;
/**
* 创建一个Axios的新实例
*
* @param {Object} instanceConfig 实例的配置
*/
function Axios(instanceConfig) {
// 存储配置和请求响应拦截器
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager(),
};
}

/**
* 原型上的发送请求方法
*
* @param {Object} config 请求时的配置(已经与默认配置合并)
*/
Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// 当config为字符串时,允许axios('example/url'[, config])这种风格的请求
if (typeof config === 'string') {
// 从arguments中取参数
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}

// 合并手动和默认配置
config = mergeConfig(this.defaults, config);

// 设置config上的method属性,优先手动,次之默认,如果都没配置就选get
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}

// 断言版本问题
var transitional = config.transitional;

if (transitional !== undefined) {
validator.assertOptions(
transitional,
{
silentJSONParsing: validators.transitional(validators.boolean, '1.0.0'),
forcedJSONParsing: validators.transitional(validators.boolean, '1.0.0'),
clarifyTimeoutError: validators.transitional(
validators.boolean,
'1.0.0'
),
},
false
);
}

// 过滤拦截器
// 设一个空数组来保存全部的请求拦截器
var requestInterceptorChain = [];
// 拦截是否异步的标志
var synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(
interceptor
) {
// 时机不对,不能添加到拦截器链中
if (
typeof interceptor.runWhen === 'function' &&
interceptor.runWhen(config) === false
) {
return;
}

// 只要有一个不是同步,就设为false
synchronousRequestInterceptors =
synchronousRequestInterceptors && interceptor.synchronous;
// 保存拦截器
requestInterceptorChain.unshift(
interceptor.fulfilled,
interceptor.rejected
);
});

// 同样设一个空数组来保存全部的响应拦截器
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(
interceptor
) {
// 保存拦截器
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});

var promise;

// 如果含有异步的拦截器,则进行异步的处理方式
if (!synchronousRequestInterceptors) {
// 把选择适配器发送请求的方法放在chain数组的最后
var chain = [dispatchRequest, undefined];

// 请求拦截器放到数组前面
Array.prototype.unshift.apply(chain, requestInterceptorChain);
// 响应拦截器放到数组后面
chain.concat(responseInterceptorChain);

// 将config作为promise链的第一个
promise = Promise.resolve(config);
// 循环执行promise
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}

return promise;
}

// 如果没有异步拦截器,则进行同步处理方式
var newConfig = config;
// 循环请求拦截器
while (requestInterceptorChain.length) {
var onFulfilled = requestInterceptorChain.shift();
var onRejected = requestInterceptorChain.shift();
// 执行拦截器的fulfilled
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected(error);
break;
}
}

// 将请求派发出去
try {
promise = dispatchRequest(newConfig);
} catch (error) {
return Promise.reject(error);
}

// 循环响应拦截器
while (responseInterceptorChain.length) {
promise = promise.then(
responseInterceptorChain.shift(),
responseInterceptorChain.shift()
);
}

// 最后把结果返回
return promise;
};

/**
* @description: 通过config的配置组装url
* @param {Object} config 配置
* @return {String} 返回拼装成的uri
*/
Axios.prototype.getUri = function getUri(config) {
config = mergeConfig(this.defaults, config);
return buildURL(config.url, config.params, config.paramsSerializer).replace(
/^\?/,
''
);
};

// 给每个method添加别名,这四个方法有可能是没有data的
utils.forEach(
['delete', 'get', 'head', 'options'],
function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function (url, config) {
return this.request(
mergeConfig(config || {}, {
method: method,
url: url,
data: (config || {}).data,
})
);
};
}
);

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

module.exports = Axios;

总结

本篇分析了 /core 目录下的核心工具方法,逐步接近了最核心的 Axios 类。

下一篇 Axios 源码解析(六):入口文件 来解析入口文件 axios.js,直达最终目标。

👆 全文结束,棒槌时间到 👇