# xhr 请求处理函数
在前面的章节dispatchRequest 函数分析中,我们得知dispatchRequest函数通过不同的环境来选择不同的请求处理函数去发送请求。那么,本章节,我们就来分析浏览器环境的 xhr 请求处理函数
# 源码分析
我们先来分析一下源码,源码是在lib/adapters/xhr.js文件
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");
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// 请求数据
var requestData = config.data;
// 请求头
var requestHeaders = config.headers;
if (utils.isFormData(requestData)) {
// 如果请求数据是`FormData`类型的,需要删除`Content-Type`请求头,
// 否则浏览器会不知道你所发送的数据类型为`FormData`类型
delete requestHeaders["Content-Type"];
}
// 创建`request`对象
var request = new XMLHttpRequest();
// http身份验证,采用的是 基本认证(Basic access authentication)
// 只适用于`HTTP Basic auth`,`Bearer`需要自己去定义`Authorization`请求头
if (config.auth) {
// 开启之后,会设置`Authorization`请求头,如果已经存在,会被覆盖
// 用户名
var username = config.auth.username || "";
// 密码
var password = config.auth.password
? unescape(encodeURIComponent(config.auth.password))
: "";
// 加密策略:用户名和密码用`:`合并,将合并后的字符串使用BASE64加密为密文,然后在前面添加`Basic `
requestHeaders.Authorization = "Basic " + btoa(username + ":" + password);
}
// 根据`baseURL`和`url`拼接完整的请求地址
var fullPath = buildFullPath(config.baseURL, config.url);
// 准备发送请求,`buildURL`函数会构建出一个带有查询参数的完整url地址
request.open(
config.method.toUpperCase(),
buildURL(fullPath, config.params, config.paramsSerializer),
true
);
// 设置请求超时时间
request.timeout = config.timeout;
// 监听onreadystatechange事件
request.onreadystatechange = function handleLoad() {
// 0:未初始化,还没有调用send()方法;
// 1:载入,已调用send()方法,正在发送请求;
// 2:载入完成,send()方法执行完成,已经接收到全部响应内容;
// 3:交互,正在解析响应内容;
// 4:完成,响应内容解析完成,可以在客户端进行调用了;
if (!request || request.readyState !== 4) {
return;
}
// status=0说明还没初始化,就是还没调用send()方法
// 会在`onerror`和`ontimeout`事件之前变成status=0
if (
request.status === 0 &&
!(request.responseURL && request.responseURL.indexOf("file:") === 0)
) {
return;
}
// 获取响应头,并转化为json对象
var responseHeaders =
"getAllResponseHeaders" in request
? parseHeaders(request.getAllResponseHeaders())
: null;
// 获取响应数据
var responseData =
!config.responseType || config.responseType === "text"
? request.responseText
: request.response;
// 构建响应对象
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request,
};
// settle函数是根据返回的状态码来判断请求是否成功,
// 然后调用resolve/reject
settle(resolve, reject, response);
// 置空 `request` 对象
request = null;
};
// 监听 `onabort` 事件,即请求取消事件
request.onabort = function handleAbort() {
if (!request) {
return;
}
// 返回一个经过处理的error对象
reject(createError("Request aborted", config, "ECONNABORTED", request));
// 置空 `request` 对象
request = null;
};
// 网络错误
request.onerror = function handleError() {
reject(createError("Network Error", config, null, request));
request = null;
};
// 请求超时
request.ontimeout = function handleTimeout() {
// 超时错误信息
var timeoutErrorMessage = "timeout of " + config.timeout + "ms exceeded";
if (config.timeoutErrorMessage) {
// 如果传入了`config.timeoutErrorMessage`,就使用`config.timeoutErrorMessage`的
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(createError(timeoutErrorMessage, config, "ECONNABORTED", request));
request = null;
};
// 防御 xsrf
// 需要在标准浏览器中才能使用,如果是在react-native等非标准浏览器中就不能使用了
if (utils.isStandardBrowserEnv()) {
// 默认值
// {
// xsrfCookieName: 'XSRF-TOKEN',
// xsrfHeaderName: 'X-XSRF-TOKEN',
// }
// 这段代码的逻辑很简单。如果 cookie 中包含 XSRF-TOKEN 这个字段,
// 就把 header 中 X-XSRF-TOKEN 字段的值设为 XSRF-TOKEN 对应的值
// `withCredentials`配置参数为`true`并且是同源请求
// isURLSameOrigin涉及到一些知识点,需要重点分析
var xsrfValue =
(config.withCredentials || isURLSameOrigin(fullPath)) &&
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"
) {
// 空数据需要删除`content-type`请求头
delete requestHeaders[key];
} else {
// 调用`setRequestHeader`设置请求头
request.setRequestHeader(key, val);
}
});
}
// 设置跨域的时候是否携带cookie,同域的时候不管设置或者不设置,效果都是一样的
if (!utils.isUndefined(config.withCredentials)) {
request.withCredentials = !!config.withCredentials;
}
// 设置响应数据类型
if (config.responseType) {
try {
request.responseType = config.responseType;
} catch (e) {
// 浏览器引发的预期DomeException与XMLHttpRequest级别2不兼容。
// 但是,对于`json`类型,这可以被限制,因为它可以通过默认的`transformResponse`函数进行解析。
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 (config.cancelToken) {
// cancelToken对象,该对象上面会存在一个promise实例
// 一旦promise实例变成成功状态,就会来到`then`函数这里
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
// 中断请求
request.abort();
reject(cancel);
request = null;
});
}
if (!requestData) {
// 请求数据不存在的情况下,置为null
requestData = null;
}
// 发送请求
request.send(requestData);
});
};
# 执行流程
从上面的源码分析中,我们可以分析出 xhr 请求处理函数的执行过程,如下:
1、判断请求数据是否为FormData类型的,如果是,则需要删除Content-Type请求头,否则浏览器识别不了你所发送的数据类型为FormData类型
2、创建一个XMLHttpRequest实例
3、如果开启了 http 身份验证(采用Basic access authentication),则根据用户名和密码构建请求头Authorization的 value 值
4、根据baseURL和url拼接完整的请求地址
5、调用XMLHttpRequest实例open方法,准备发起请求
6、设置请求超时时间(timeout),跨域是否携带 cookie(withCredentials)
7、根据xsrfCookieName字段和xsrfHeaderName来开启xsrf防御
8、遍历config.headers字段,调用XMLHttpRequest实例的setRequestHeader方法设置请求头
9、设置responseType数据响应类型
10、监听onreadystatechange状态码变化事件,onabort请求中断事件,onerror网络错误事件,ontimeout请求超时事件,progress下载文件事件(如果用户传入onDownloadProgress配置项才监听),upload对象progress上传文件事件(如果用户传入onUploadProgress配置项并且upload对象存在才监听)
11、如果用户传入了cancelToken实例(用来取消请求的,后面的章节会讲解),则调用cancelToken实例上面的 promise 属性的then方法来监听用户是否在外部取消了请求
12、调用XMLHttpRequest实例send方法,并传入请求数据,发送请求
xhr 请求处理函数的执行过程优有点复杂,但是整体上可以归纳为:
1、创建XMLHttpRequest实例
2、设置请求头
3、监听对应的事件函数
4、发送请求
# 功能点分析
从上面的分析中,我们可以看见 xhr 请求处理函数有几个强大的功能点,分别如下:
1、http 身份验证,只支持Basic access authentication
2、客户端防御 XSRF。只能在标准浏览器中使用,react-native,ns等非标准浏览器中使用不了
3、取消请求,通过CancelToken类实现
上面的三个功能点,以及实现原理,过程,我们后面会有专门的章节进行讲解,这里不细讲
# isURLSameOrigin 函数
isURLSameOrigin函数是用来判断请求地址是否同源,这个函数我们会单独用一个章节来分析,因为这里面涉及到的知识点比较新颖
# onreadystatechange 事件分析
当请求的状态码发生变化时,就会触发onreadystatechange事件。请求的状态码一共分为五种,分别如下:
0:未初始化,还没有调用 send()方法;
1:载入,已调用 send()方法,正在发送请求;
2:载入完成,send()方法执行完成,已经接收到全部响应内容;
3:交互,正在解析响应内容;
4:完成,响应内容解析完成,可以在客户端进行调用了;
我们只需要关心状态为4的情况,其余四种情况不做考虑
当状态为4的时候,也就是请求完成了,此时,会构建出一个response响应对象,该对象包含data响应数据,status响应状态码,statusText响应状态文本,headers响应头,config请求配置项,request请求实例
最后我们看见调用了settle函数,我们来看一下该函数的内部实现,源码在/lib/core/settle.js
var createError = require("./createError");
/**
* 根据响应状态码,调用resolve或者reject
*
* @param {Function} resolve A function that resolves the promise.
* @param {Function} reject A function that rejects the promise.
* @param {object} response The response.
*/
module.exports = function settle(resolve, reject, response) {
var validateStatus = response.config.validateStatus;
// 使用validateStatus函数校验是否为成功状态
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
// 失败就返回一个经过处理的error对象
reject(
createError(
"Request failed with status code " + response.status,
response.config,
null,
response.request,
response
)
);
}
};
我们可以看见settle函数根据响应状态码,以及validateStatus校验状态码来判断请求是否成功。
那么,我们在来看看validateStatus函数的默认配置项,源码在/lib/defaults.js文件,第 113 行
var defaults = {
// 请求成功或者失败的校验函数,传入的参数是状态码
validateStatus: function validateStatus(status) {
// 大于等于200小于300就是成功
return status >= 200 && status < 300;
},
};
通过validateStatus函数的分析,我们可以的出的结论是,如果响应状态码大于等于 200 并且小于 300 的,则请求是成功的,否则,请求是失败的
# 其他事件
其他一些事件,比如ontimeout,onerror,onabort等事件,请求都是被认为是失败的,此时会通过createError函数,创建一个Error对象实例并返回,该对象实例是经过处理的,添加了一些额外属性
# 注意点
在分析出 xhr 请求处理函数的过程中,我们有些地方需要特别注意一下
1、请求数据是FormData类型的,需要删除Content-Type请求头,浏览器会自动识别的
2、withCredentials属性是用来设置跨域的时候是否携带 cookie,同域的时候不管设置或者不设置,效果都是一样的
3、上传文件事件并不是所有浏览器都支持的,需要判断请求实例中是否有upload事件
4、将 responseType 设置为特定值时,应确保服务器实际发送的响应与该格式兼容。如果服务器返回的数据与设置的 responseType 不兼容,则 response 的值将为 null
5、xsrf 防御只在客户端有效,也就是标准浏览器环境。其他非标准浏览器环境,比如react-native等,或者其他环境,比如 node,是无效的,因为不存在cookie这种说法的。
6、上传文件和下载文件监听的事件都是叫progress,但是监听的对象是不一样的
# 总结
通过对 xhr 请求处理函数的分析,我们了解到了 axios 的一些常见的功能,比如取消请求、客户端防御 XSRF、http 身份验证。同时也对XMLHttpRequest怎么发送请求有了一定的了解。以及一些常见的事件,还有请求状态码等等
在下一个章节,我们将会讲解 http 身份验证(auth 身份验证)功能的实现以及原理