叁:dispatchRequest和adapter

书接上回,上节课的内容留下了一个坑,dispatchRequest方法内部究竟做了什么?这一节我们就来看一下其内部实现。

关于取消请求部分的逻辑在这一节会省略掉,以求最简单清晰的去理解整个工作流程。

dispatchRequest

dispatchRequest这里我们主要还是对于整体工作流程的一个把控,对于取消请求的内容会进行省略。在该方法中其实也没有真正的发送请求,真正的XMLHttpRequest部分是在适配器中做的,我们会在说完dispatchRequest方法之后再来看一下adapter中的具体实现。

config处理

Axios类的request方法中,我们对于请求拦截器一直在传递一个config,毫无疑问在dispatchRequest方法中也会接收到这个config参数。所以先依次来看一下对于config到处理:

1
2
3
if (config.baseURL && !isAbsoluteURL(config.url)) {
config.url = combineURLs(config.baseURL, config.url);
}

isAbsoluteURL这个方法就是通过正则来判断是否是一个绝对路由,内容只有一句return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);

如果config.baseURL存在且config.url不是一个绝对路由,就对两者进行拼接,逻辑就是如果baseURL最后一个字符是/就会去掉,如果url到第一个字符是/也会去掉,然后通过在两者中间加一个/将两者拼起来,代码如下:

1
2
3
4
5
6
// combineURLs
function combineURLs(baseURL, relativeURL) {
return relativeURL
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
: baseURL;
};

然后是对于data的一些处理:

1
2
3
4
5
6
config.headers = config.headers || {};
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);

transformRequest我们在默认配置那一章节就已经看到了,通过调用transformData方法,把dataheaders依次作为transformRequest中每一个方法的参数执行,返回转换后的数据。

1
2
3
4
5
6
7
8
// transformData
function transformData(data, headers, fns) {
utils.forEach(fns, function transform(fn) {
data = fn(data, headers);
});

return data;
};

然后把headers的内容合并,注意后边的参数的优先级会更高:

1
2
3
4
5
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers || {}
);

我们在默认配置中在headers为每一个方法创建了一个对象存储一些属性,在发送请求的时候这是没有用的,所以要删除掉。

1
2
3
4
5
6
utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);

adapter使用

处理完了config就该发送请求了,记得我们在默认配置中设置了适配器,如果没有传递就会使用这个默认的适配器,大家平常使用的时候应该也不会传递适配器把。

1
var adapter = config.adapter || defaults.adapter;

这里简单提一嘴适配器adapter,方便阅读下边的代码,adapter接收config作为参数,并返回一个Promise,所以我们在这里传入了一个成功的回调和一个失败的回调,并通过transformData来调用transformResponse把结果返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
return adapter(config).then(function onAdapterResolution(response) {
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
return Promise.reject(reason);
});

注意,前边我们说过,这里我们省略了关于取消请求的相关内容,在下边的dispathRequest完整源码中可以忽略相关内容,我们会在后边单独讲解取消请求的相关内容。

dispatchRequest源码

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
// dispatchRequest.js

'use strict';

var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
var isAbsoluteURL = require('./../helpers/isAbsoluteURL');
var combineURLs = require('./../helpers/combineURLs');

/**
* Throws a `Cancel` if cancellation has been requested.
*/
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}

/**
* Dispatch a request to the server using the configured adapter.
*
* @param {object} config The config that is to be used for the request
* @returns {Promise} The Promise to be fulfilled
*/
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);

// Support baseURL config
if (config.baseURL && !isAbsoluteURL(config.url)) {
config.url = combineURLs(config.baseURL, config.url);
}

// Ensure headers exist
config.headers = config.headers || {};

// Transform request data
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);

// Flatten headers
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers || {}
);

utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);

var adapter = config.adapter || defaults.adapter;

return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);

// Transform response data
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);

return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);

// Transform response data
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}

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

adapter - xhr

主线任务马上就要结束了哦,前边我们说过适配器分为两种,我们这里只关注适用于浏览器端的xhr

adapter接收config并返回一个promise,前边刚说过,我们就直接来看函数内部的处理逻辑。

config处理

是的,这里还是进行了一些额外处理,其实就是判断了一些如果数据是formData就去掉请求头中的Contetn-Type让浏览器来设置它。

1
2
3
4
5
6
7
8
9
10
11
var requestData = config.data;
var requestHeaders = config.headers;
if (utils.isFormData(requestData)) {
delete requestHeaders['Content-Type']; // Let the browser set it
}

if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password || '';
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}

请求头和请求数据

对于请求头的处理直接看代码吧,比较容易理解

同样省略了取消请求部分

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
if (utils.isStandardBrowserEnv()) {
var cookies = require('./../helpers/cookies');

// Add xsrf header
var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;

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

if ('setRequestHeader' in request) {
utils.forEach(requestHeaders, function setRequestHeader(val, key) {
if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
// Remove Content-Type if data is undefined
delete requestHeaders[key];
} else {
// Otherwise add header to the request
request.setRequestHeader(key, val);
}
});
}

if (config.withCredentials) {
request.withCredentials = true;
}
if (config.responseType) {
try {
request.responseType = config.responseType;
} catch (e) {
if (config.responseType !== 'json') {
throw e;
}
}
}

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

if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}

if (requestData === undefined) {
requestData = null;
}

XMLHttpRequest

接下来就是创建XMLHttpRequest准备发送请求,还有为了适配IE 8/9做的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var request = new XMLHttpRequest();
var loadEvent = 'onreadystatechange';
var xDomain = false;

if (process.env.NODE_ENV !== 'test' &&
typeof window !== 'undefined' &&
window.XDomainRequest && !('withCredentials' in request) &&
!isURLSameOrigin(config.url)) {
request = new window.XDomainRequest();
loadEvent = 'onload';
xDomain = true;
request.onprogress = function handleProgress() { };
request.ontimeout = function handleTimeout() { };
}

调用xhr.open方法, buildURL方法是为了拼接params参数,不单独拿出来说了。

1
request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true);

设置超时时间:

1
request.timeout = config.timeout;

重头戏肯定是onreadystatechange事件啦,直接在代码中写注释啦。

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
request[loadEvent] = function handleLoad() {
if (!request || (request.readyState !== 4 && !xDomain)) {
return;
}
// 无响应
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}

// 开始准备响应结果
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
// ajax响应数据
var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
// 生成返回结果
var response = {
data: responseData,
// IE浏览器就用1223代替204状态码
status: request.status === 1223 ? 204 : request.status,
statusText: request.status === 1223 ? 'No Content' : request.statusText,
headers: responseHeaders,
config: config,
request: request
};

settle(resolve, reject, response);

// 清空request
request = null;
};

我们看到对于返回内容是通过settle方法来处理的,我们来单独看一下这个方法做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function settle(resolve, reject, response) {
var validateStatus = response.config.validateStatus;
// 通过验证就返回response
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
// 不然就抛出一个错误
reject(createError(
'Request failed with status code ' + response.status,
response.config,
null,
response.request,
response
));
}
};

看完这一段代码应该知道后端返回的结果为什么在data中才能取到哦

对于网络错误和超时的处理就是直接抛出了错误,就直接放代码了:

1
2
3
4
5
6
7
8
9
10
request.onerror = function handleError() {
reject(createError('Network Error', config, null, request));
request = null;
};

request.ontimeout = function handleTimeout() {
reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',
request));
request = null;
};

最后的最后,发送请求,大功告成~

1
request.send(requestData);

注意,源码的书写顺序与这里并不一致,但是不会影响理解源码内容。

接下来放出完整源码,一样可以忽略取消请求的内容哈。

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
'use strict';

var utils = require('./../utils');
var settle = require('./../core/settle');
var buildURL = require('./../helpers/buildURL');
var parseHeaders = require('./../helpers/parseHeaders');
var isURLSameOrigin = require('./../helpers/isURLSameOrigin');
var createError = require('../core/createError');
var btoa = (typeof window !== 'undefined' && window.btoa && window.btoa.bind(window)) || require('./../helpers/btoa');

module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var requestHeaders = config.headers;

if (utils.isFormData(requestData)) {
delete requestHeaders['Content-Type']; // Let the browser set it
}

var request = new XMLHttpRequest();
var loadEvent = 'onreadystatechange';
var xDomain = false;

// For IE 8/9 CORS support
// Only supports POST and GET calls and doesn't returns the response headers.
// DON'T do this for testing b/c XMLHttpRequest is mocked, not XDomainRequest.
if (process.env.NODE_ENV !== 'test' &&
typeof window !== 'undefined' &&
window.XDomainRequest && !('withCredentials' in request) &&
!isURLSameOrigin(config.url)) {
request = new window.XDomainRequest();
loadEvent = 'onload';
xDomain = true;
request.onprogress = function handleProgress() { };
request.ontimeout = function handleTimeout() { };
}

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

request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true);

// Set the request timeout in MS
request.timeout = config.timeout;

// Listen for ready state
request[loadEvent] = function handleLoad() {
if (!request || (request.readyState !== 4 && !xDomain)) {
return;
}

// The request errored out and we didn't get a response, this will be
// handled by onerror instead
// With one exception: request that using file: protocol, most browsers
// will return status as 0 even though it's a successful request
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}

// Prepare the response
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
var response = {
data: responseData,
// IE sends 1223 instead of 204 (https://github.com/axios/axios/issues/201)
status: request.status === 1223 ? 204 : request.status,
statusText: request.status === 1223 ? 'No Content' : request.statusText,
headers: responseHeaders,
config: config,
request: request
};

settle(resolve, reject, response);

// Clean up request
request = null;
};

// Handle low level network errors
request.onerror = function handleError() {
// Real errors are hidden from us by the browser
// onerror should only fire if it's a network error
reject(createError('Network Error', config, null, request));

// Clean up request
request = null;
};

// Handle timeout
request.ontimeout = function handleTimeout() {
reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',
request));

// Clean up request
request = null;
};

// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (utils.isStandardBrowserEnv()) {
var cookies = require('./../helpers/cookies');

// Add xsrf header
var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;

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

// Add headers to the request
if ('setRequestHeader' in request) {
utils.forEach(requestHeaders, function setRequestHeader(val, key) {
if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
// Remove Content-Type if data is undefined
delete requestHeaders[key];
} else {
// Otherwise add header to the request
request.setRequestHeader(key, val);
}
});
}

// Add withCredentials to request if needed
if (config.withCredentials) {
request.withCredentials = true;
}

// Add responseType to request if needed
if (config.responseType) {
try {
request.responseType = config.responseType;
} catch (e) {
// Expected DOMException thrown by browsers not compatible XMLHttpRequest Level 2.
// But, this can be suppressed for 'json' type as it can be parsed by default 'transformResponse' function.
if (config.responseType !== 'json') {
throw e;
}
}
}

// Handle progress if needed
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}

// Not all browsers support upload events
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}

if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}

request.abort();
reject(cancel);
// Clean up request
request = null;
});
}

if (requestData === undefined) {
requestData = null;
}

// Send the request
request.send(requestData);
});
};

恭喜,主线任务完成了🎉🎉🎉

作者

胡兆磊

发布于

2022-10-18

更新于

2022-10-24

许可协议