0%

Axios源码解析(三):适配器

上篇 Axios 源码解析(二):通用工具方法 解析了通用工具方法部分的源码。下面继续解析适配器部分的代码,也就是 /adapters 目录。

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

/adapters

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

1
2
3
4
├─adapters
│ http.js
│ README.md
│ xhr.js

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

adapters/ 下的模块负责发送请求并在收到响应后处理返回的 Promise 。也就是整个 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
var settle = require('./../core/settle');

module.exports = function myAdapter(config) {
// 在此时:
// - 配置已与默认配置合并
// - 请求转换器已经执行
// - 请求拦截器已经执行

/*
* ------
*/
// 使用提供的配置发出请求
// 根据响应来 settle Promise
return new Promise(function (resolve, reject) {
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request,
};

settle(resolve, reject, response);
/*
* ------
*/

// 从这里开始:
// - 响应转换器开始执行
// - 响应拦截器开始执行
});
};

由于 Axios 的使用环境其实就是两种,一种是在浏览器端发送 XHR 请求,另一种是在 nodejs 中发送 http 请求。所以 Axios 的适配器只有两个:http.jsxhr.js

xhr.js

在浏览器环境中,Axios 直接封装的 XMLHttpRequest,流程大致如下所示:

  1. 新建一个 XHR 对象
  2. 解析 URL
  3. 处理 dataheadersresponseType
  4. 设置超时时间
  5. 打开请求
  6. 添加 onloadendonloadendonabortonerrorontimeoutonUploadProgress 事件
  7. 发送请求

xhr 适配器的具体代码如下:

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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
'use strict';

var utils = require('./../utils');
var settle = require('./../core/settle');
var cookies = require('./../helpers/cookies');
var buildURL = require('./../helpers/buildURL');
var buildFullPath = require('../core/buildFullPath');
var parseHeaders = require('./../helpers/parseHeaders');
var isURLSameOrigin = require('./../helpers/isURLSameOrigin');
var createError = require('../core/createError');

/**
* @description: 浏览器环境中使用XHR对象来发送请求
* @param {Object} config 已经合并并且标准化后的配置对象
* @return {Promise} 返回一个promise对象
*/
module.exports = function xhrAdapter(config) {
// 标准的新建Promise对象的写法
return new Promise(function dispatchXhrRequest(resolve, reject) {
// 拿到data,headers,和responseType
var requestData = config.data;
var requestHeaders = config.headers;
var responseType = config.responseType;

if (utils.isFormData(requestData)) {
delete requestHeaders['Content-Type']; // 删掉content-type,让浏览器来设置
}

// 新建一个XHR对象
var request = new XMLHttpRequest();

// HTTP basic 认证
if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password
? unescape(encodeURIComponent(config.auth.password))
: '';
// 编码base64字符串,构造出一个Authorization
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}

// 构造全路径
var fullPath = buildFullPath(config.baseURL, config.url);
// 打开请求
request.open(
config.method.toUpperCase(),
buildURL(fullPath, config.params, config.paramsSerializer),
true
);

// 设置毫秒级的超时时间限制
request.timeout = config.timeout;

/**
* @description: 设置loadend的回调
*/
function onloadend() {
if (!request) {
return;
}
// 响应头处理
var responseHeaders =
'getAllResponseHeaders' in request
? parseHeaders(request.getAllResponseHeaders())
: null;
// 响应内容处理
var responseData =
!responseType || responseType === 'text' || responseType === 'json'
? request.responseText
: request.response;
// 构造出response
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request,
};

// 调用settle方法来处理promise
settle(resolve, reject, response);

// 清空request
request = null;
}

// 如果request上有onloadend属性,则直接替换
if ('onloadend' in request) {
request.onloadend = onloadend;
} else {
// 否则就用onreadystatechange来模拟onloadend
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}

// 请求出错,我们没有得到响应,这将由 onerror 处理。
// 但只有一个例外:请求使用 file:协议,此时即使它是一个成功的请求,大多数浏览器也将返回状态为 0,
if (
request.status === 0 &&
!(request.responseURL && request.responseURL.indexOf('file:') === 0)
) {
return;
}
// readystate 处理器在 onerror 或 ontimeout处理器之前调用, 因此我们应该在next 'tick' 上调用onloadend
setTimeout(onloadend);
};
}

// 处理浏览器对request的取消(与手动取消不同)
request.onabort = function handleAbort() {
if (!request) {
return;
}

reject(createError('Request aborted', config, 'ECONNABORTED', request));

// 清空request
request = null;
};

// 处理更低级别的网络错误
request.onerror = function handleError() {
// 真正的错误被浏览器掩盖了
// onerror应当只可被网络错误触发
reject(createError('Network Error', config, null, request));

// 清空request
request = null;
};

// 处理超时
request.ontimeout = function handleTimeout() {
var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(
createError(
timeoutErrorMessage,
config,
config.transitional && config.transitional.clarifyTimeoutError
? 'ETIMEDOUT'
: 'ECONNABORTED',
request
)
);

// 清空request
request = null;
};

// 添加 xsrf 头
// 只能在浏览器环境中生效
// 在工作者线程或者RN中不生效
if (utils.isStandardBrowserEnv()) {
// 添加 xsrf 头
var xsrfValue =
(config.withCredentials || isURLSameOrigin(fullPath)) &&
config.xsrfCookieName
? cookies.read(config.xsrfCookieName)
: undefined;

if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}

// 给request添加headers
if ('setRequestHeader' in request) {
utils.forEach(requestHeaders, function setRequestHeader(val, key) {
if (
typeof requestData === 'undefined' &&
key.toLowerCase() === 'content-type'
) {
// 如果data是undefined,则移除Content-Type
delete requestHeaders[key];
} else {
// 否则把header添加给request
request.setRequestHeader(key, val);
}
});
}

// 添加withCredentials
if (!utils.isUndefined(config.withCredentials)) {
request.withCredentials = !!config.withCredentials;
}

// 添加 responseType
if (responseType && responseType !== 'json') {
request.responseType = config.responseType;
}

// 处理progess
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}

// 不是所有的浏览器都支持上传事件
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}

// 处理手动取消
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}

request.abort();
reject(cancel);
// 清空request
request = null;
});
}

if (!requestData) {
requestData = null;
}

// 发送请求
request.send(requestData);
});
};

http.js

nodejs 环境中,Axios 封装的是 http 库,流程大致如下所示:

  1. 转换数据格式
  2. 处理代理
  3. 解析 URL
  4. 创建请求
  5. 添加 errorenddata 等事件
  6. 发送请求

http 适配器的具体代码如下:

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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
'use strict';

var utils = require('./../utils');
var settle = require('./../core/settle');
var buildFullPath = require('../core/buildFullPath');
var buildURL = require('./../helpers/buildURL');
var http = require('http');
var https = require('https');
var httpFollow = require('follow-redirects').http;
var httpsFollow = require('follow-redirects').https;
var url = require('url');
var zlib = require('zlib');
var pkg = require('./../../package.json');
var createError = require('../core/createError');
var enhanceError = require('../core/enhanceError');

var isHttps = /https:?/;

/**
* @description 设置代理用的方法
* @param {http.ClientRequestArgs} options
* @param {AxiosProxyConfig} proxy
* @param {string} location
*/
function setProxy(options, proxy, location) {
options.hostname = proxy.host;
options.host = proxy.host;
options.port = proxy.port;
options.path = location;

// basic形式的Proxy-Authorization头
if (proxy.auth) {
var base64 = Buffer.from(
proxy.auth.username + ':' + proxy.auth.password,
'utf8'
).toString('base64');
options.headers['Proxy-Authorization'] = 'Basic ' + base64;
}

// 如果使用了代理,那么重定向时必须要经过代理
options.beforeRedirect = function beforeRedirect(redirection) {
redirection.headers.host = redirection.host;
setProxy(redirection, proxy, redirection.href);
};
}

/*eslint consistent-return:0*/
module.exports = function httpAdapter(config) {
return new Promise(function dispatchHttpRequest(
resolvePromise,
rejectPromise
) {
var resolve = function resolve(value) {
resolvePromise(value);
};
var reject = function reject(value) {
rejectPromise(value);
};
var data = config.data;
var headers = config.headers;

// 设置 User-Agent (某些服务端强制要求)
// 详见 https://github.com/axios/axios/issues/69
if ('User-Agent' in headers || 'user-agent' in headers) {
// 当不需要UA头时
if (!headers['User-Agent'] && !headers['user-agent']) {
delete headers['User-Agent'];
delete headers['user-agent'];
}
// 当需要UA头时就指定一个
} else {
// 只有在config中没有指定UA时才设置
headers['User-Agent'] = 'axios/' + pkg.version;
}

// 转换数据格式
if (data && !utils.isStream(data)) {
if (Buffer.isBuffer(data)) {
// 什么都不做...
} else if (utils.isArrayBuffer(data)) {
data = Buffer.from(new Uint8Array(data));
} else if (utils.isString(data)) {
data = Buffer.from(data, 'utf-8');
} else {
return reject(
createError(
'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream',
config
)
);
}

// 如果data存在,则需要设置Content-Length
headers['Content-Length'] = data.length;
}

// HTTP basic authentication
var auth = undefined;
if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password || '';
auth = username + ':' + password;
}

// 解析 url
var fullPath = buildFullPath(config.baseURL, config.url);
var parsed = url.parse(fullPath);
var protocol = parsed.protocol || 'http:';

if (!auth && parsed.auth) {
var urlAuth = parsed.auth.split(':');
var urlUsername = urlAuth[0] || '';
var urlPassword = urlAuth[1] || '';
auth = urlUsername + ':' + urlPassword;
}

if (auth) {
delete headers.Authorization;
}

var isHttpsRequest = isHttps.test(protocol);
var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;

var options = {
path: buildURL(
parsed.path,
config.params,
config.paramsSerializer
).replace(/^\?/, ''),
method: config.method.toUpperCase(),
headers: headers,
agent: agent,
agents: { http: config.httpAgent, https: config.httpsAgent },
auth: auth,
};

if (config.socketPath) {
options.socketPath = config.socketPath;
} else {
options.hostname = parsed.hostname;
options.port = parsed.port;
}

var proxy = config.proxy;
if (!proxy && proxy !== false) {
var proxyEnv = protocol.slice(0, -1) + '_proxy';
var proxyUrl =
process.env[proxyEnv] || process.env[proxyEnv.toUpperCase()];
if (proxyUrl) {
var parsedProxyUrl = url.parse(proxyUrl);
var noProxyEnv = process.env.no_proxy || process.env.NO_PROXY;
var shouldProxy = true;

if (noProxyEnv) {
var noProxy = noProxyEnv.split(',').map(function trim(s) {
return s.trim();
});

shouldProxy = !noProxy.some(function proxyMatch(proxyElement) {
if (!proxyElement) {
return false;
}
if (proxyElement === '*') {
return true;
}
if (
proxyElement[0] === '.' &&
parsed.hostname.substr(
parsed.hostname.length - proxyElement.length
) === proxyElement
) {
return true;
}

return parsed.hostname === proxyElement;
});
}

if (shouldProxy) {
proxy = {
host: parsedProxyUrl.hostname,
port: parsedProxyUrl.port,
protocol: parsedProxyUrl.protocol,
};

if (parsedProxyUrl.auth) {
var proxyUrlAuth = parsedProxyUrl.auth.split(':');
proxy.auth = {
username: proxyUrlAuth[0],
password: proxyUrlAuth[1],
};
}
}
}
}

// 如果代理存在时要进行专门处理
if (proxy) {
options.headers.host =
parsed.hostname + (parsed.port ? ':' + parsed.port : '');
setProxy(
options,
proxy,
protocol +
'//' +
parsed.hostname +
(parsed.port ? ':' + parsed.port : '') +
options.path
);
}

var transport;
var isHttpsProxy =
isHttpsRequest && (proxy ? isHttps.test(proxy.protocol) : true);
if (config.transport) {
transport = config.transport;
} else if (config.maxRedirects === 0) {
transport = isHttpsProxy ? https : http;
} else {
if (config.maxRedirects) {
options.maxRedirects = config.maxRedirects;
}
transport = isHttpsProxy ? httpsFollow : httpFollow;
}

if (config.maxBodyLength > -1) {
options.maxBodyLength = config.maxBodyLength;
}

// 创建 request
var req = transport.request(options, function handleResponse(res) {
if (req.aborted) return;

// 在需要的情况下,自动解压响应体
var stream = res;

// 如果重定向,则返回最后一次请求的信息
var lastRequest = res.req || req;

// 如果没有内容, HEAD请求禁止解压
if (
res.statusCode !== 204 &&
lastRequest.method !== 'HEAD' &&
config.decompress !== false
) {
switch (res.headers['content-encoding']) {
/*eslint default-case:0*/
case 'gzip':
case 'compress':
case 'deflate':
// 给处理流程中添加未压缩的body stream
stream = stream.pipe(zlib.createUnzip());

// 移除content-encoding, 目的是避免拒绝下载操作
delete res.headers['content-encoding'];
break;
}
}

var response = {
status: res.statusCode,
statusText: res.statusMessage,
headers: res.headers,
config: config,
request: lastRequest,
};

if (config.responseType === 'stream') {
response.data = stream;
settle(resolve, reject, response);
} else {
var responseBuffer = [];
var totalResponseBytes = 0;
stream.on('data', function handleStreamData(chunk) {
responseBuffer.push(chunk);
totalResponseBytes += chunk.length;

// 确保内容长度不超过指定的最大长度
if (
config.maxContentLength > -1 &&
totalResponseBytes > config.maxContentLength
) {
stream.destroy();
reject(
createError(
'maxContentLength size of ' +
config.maxContentLength +
' exceeded',
config,
null,
lastRequest
)
);
}
});

stream.on('error', function handleStreamError(err) {
if (req.aborted) return;
reject(enhanceError(err, config, null, lastRequest));
});

stream.on('end', function handleStreamEnd() {
var responseData = Buffer.concat(responseBuffer);
if (config.responseType !== 'arraybuffer') {
responseData = responseData.toString(config.responseEncoding);
if (
!config.responseEncoding ||
config.responseEncoding === 'utf8'
) {
responseData = utils.stripBOM(responseData);
}
}

response.data = responseData;
settle(resolve, reject, response);
});
}
});

// 处理错误
req.on('error', function handleRequestError(err) {
if (req.aborted && err.code !== 'ERR_FR_TOO_MANY_REDIRECTS') return;
reject(enhanceError(err, config, null, req));
});

// 处理请求超时
if (config.timeout) {
// 如果`req`接口无法处理其他类型,将强制用一个整数的超时时间。
var timeout = parseInt(config.timeout, 10);

if (isNaN(timeout)) {
reject(
createError(
'error trying to parse `config.timeout` to int',
config,
'ERR_PARSE_TIMEOUT',
req
)
);

return;
}

// 有时响应将非常缓慢,甚至没有响应,连接事件将被事件循环系统打断
// 此时触发定时器回调,在连接前将调用abort(),然后获取"socket hang up" 和ECONNRESET码
// 此时,如果出现大量的请求,nodejs会在幕后挂起一些socket。并且数目会不断增长。
// 然后这些挂起的socket将一点点占用 CPU。
// ClientRequest.setTimeout 将在指定毫秒内启动,并且可以确保连接之后触发abort()。
req.setTimeout(timeout, function handleRequestTimeout() {
req.abort();
reject(
createError(
'timeout of ' + timeout + 'ms exceeded',
config,
config.transitional && config.transitional.clarifyTimeoutError
? 'ETIMEDOUT'
: 'ECONNABORTED',
req
)
);
});
}

if (config.cancelToken) {
// 处理取消操作
config.cancelToken.promise.then(function onCanceled(cancel) {
if (req.aborted) return;

req.abort();
reject(cancel);
});
}

// 发送请求
if (utils.isStream(data)) {
data
.on('error', function handleStreamError(err) {
reject(enhanceError(err, config, null, req));
})
.pipe(req);
} else {
req.end(data);
}
});
};

总结

在工程中,把相似但又不同、且可替代的部分抽取出来,形成一个专用的模块,对外的接口统一,这是相当优秀的设计模式。

下一篇 Axios 源码解析(四):核心工具方法(1)来解析 Axios 中耦合度较高的工具方法。

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