axios - 4 - 包装 XMLHttpRequest
console.info
该系类文章旨在研究 axios 的实现 。在研究源码的基础上,去理解 axios
是如何实现 ajax
请求并更好的去使用这个库。
简述
对应文件为 lib/adapters/xhr.js
对应 axios - 1 - 默认配置 中的 default.adapter
一项,主要是包装了浏览器的 XMLHttpRequest 对象,方便调用。
代码分析
代码结构
由于 axios
库内部将原生的 promise
做了兼容处理,所以在代码中,直接使用 promise
对象。
因此,函数调用后直接返回 promise
对应代码如下:
function xhrAdapter(config){
return new Promise(function (resolve, reject){
// 处理 XMLHttpRequest 代码
// ...
})
}
函数调用时,需要传入的 cofing
对象,在 axios - 2 - 参数字段 中有具体的描述。
声明发送请求对象
由于在 IE8/9
中并没有实现标准的 XMLHttpRequest
对象,实现请求的对象为:XDomainRequest,虽然该对象已经过时,但为了兼容对大多数的浏览器,必须处理下。
对应的代码为:
var request = new XMLHttpRequest();
var loadEvent = 'onreadystatechange'; // 请求变化事件名
var xDomain = false;
if (window.XDomainRequest && !('withCredentials' in request) && !isURLSameOrigin(config.url)) {
request = new window.XDomainRequest();
loadEvent = 'onload'; // IE 下的请求发送变化的事件名
xDomain = true;
request.onprogress = function handleProgress() {
};
request.ontimeout = function handleTimeout() {
};
}
注: XDomainRequest
对象对应请求发送变化的事件名为 onload
,而 XMLHttpRequest
为 onreadystatechange
。
开启请求
调用 request
的 open
方法,开启一个 ajax
请求,这里只是开启请求,并没有发送请求。
request.open(
config.method.toUpperCase(),
buildURL(
config.url,
config.params,
config.paramsSerializer
),
true
);
buildURL
根据当前 config
中的 url & params & paramsSerializer
建立请求链接。
在 axios
中,实现 buildURL
代码对应的文件为 lib/helper/buildURL.js 。
绑定事件函数
设置超时时间以及事件函数,包括:onerror
、ontimeout
、onreadystatechange/onload
,具体的绑定函数的内容,在最后。
request.timeout = config.timeout;
request[loadEvent] = function handleLoad(){};
request.onerror = function handleError(){};
request.ontimeout = function handleTimeout(){};
确定 xsrf
添加与服务器约定好的 xsrf
头信息,防止跨站脚本攻击。
var cookies = require('./../helpers/cookies');
var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url))
&& config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
添加头信息
- 由于
XDomainRequest
并没有setRequestHeader
方法,所以这里需要先判断,避免报错。 XDomainRequest
对象并不需要设置请求头。
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 {
// 添加头信息
request.setRequestHeader(key, val);
}
});
}
添加认证以及授权信息
// 确定请求是否需要带凭证发送,和 cookies 的获取有关
if (config.withCredentials) {
request.withCredentials = true;
}
// 添加用户认证信息
if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password || '';
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
注: btoa
为 base64
加密函数,可用 window.btoa
直接获取,当 window
下没有过该对象时,需要自己实现。
在 axios
中,实现 btoa
代码对应的文件为 lib/helper/btoa.js
确定响应类型
if (config.responseType) {
try {
request.responseType = config.responseType;
} catch (e) {
if (request.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) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
// 中断请求
request.abort();
reject(cancel);
// 清空请求,释放内存
request = null;
});
}
发送请求
调用一次 send
方法即可。
request.send(requestData);
关于绑定的3个函数的内容
具体的处理过程查看代码中的注释。
onreadystatechange/onload
请求变化时
function handleLoad() {
// 请求未成功
if (!request || (request.readyState !== 4 && !xDomain)) {
return;
}
// file 协议的请求,大多数浏览器返回的状态码为 0 ,但是请求是成功的。
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
// 处理 request 获取 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,
// 正常浏览器的 204 ,但 IE 是 1223
status: request.status === 1223 ? 204 : request.status,
statusText: request.status === 1223 ? 'No Content' : request.statusText,
headers: responseHeaders,
config: config,
request: request
};
// 处理 response
settle(resolve, reject, response);
// 取消 request 释放内存
request = null;
}
注1: settle
函数调用 config
下的 validateStatus
函数(该函数需要传入 response.status
)的返回值来 resolve
或是 reject
当前的 promise
。
对应于 axios
源码中 lib/core/settle.js 文件,
settle
函数的具体处理过程
function settle(resolve, reject, response) {
var validateStatus = response.config.validateStatus;
// 在 XDomainRequest 对象中没有 status
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
reject(createError(
'Request failed with status code ' + response.status,
response.config,
null,
response
));
}
}
错误处理在 axios - 5 - 错误处理 中有具体的描述。
onerror/ontimeout
请求出错时
function handleError() {
// 处理错误信息,reject 当前 promise
reject(createError('Network Error', config));
// 取消 request 释放内存
request = null;
}
function handleTimeout() {
// 处理错误信息,reject 当前 promise
reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED'));
// 取消 request 释放内存
request = null;
}
一整个 xhr.js
中的内容,就这样了。
总结
该文件规定了一个请求发送器,返回一个 Promise
。