异步请求-XMLHttpRequest、ajax、axios原理浅析
XMLHTTPRequest
参考
readyState
值 | 状态 | 描述 |
---|---|---|
0 | UNSENT (未打开) | 表示已创建 XHR 对象,open() 方法还未被调用 |
1 | OPENED (未发送) | open() 方法已被成功调用,send() 方法还未被调用 |
2 | HEADERS_RECEIVED (已获取响应头) | send() 方法已经被调用,响应头和响应状态已经返回 |
3 | LOADING (正在下载响应体) | 响应体下载中,responseText中已经获取了部分数据 |
4 | DONE (请求完成) | 整个请求过程已经完毕 |
responseType
enum XMLHttpRequestResponseType {
"",
"arraybuffer",
"blob",
"document",
"json",
"text"
};
XMLHttpRequest Level 1
var xhr = new XMLHttpRequest();
xhr.open('GET', 'example.php');
xhr.send();
xhr.onreadystatechange = function(){
if ( xhr.readyState == 4 && xhr.status == 200 ) {
alert( xhr.responseText );
} else {
alert( xhr.statusText );
}
};
只支持文本数据的传送,无法用来读取和上传二进制文件。
传送和接收数据时,没有进度信息,只能提示有没有完成。
受到"同域限制"(Same Origin Policy),只能向同一域名的服务器请求数据。
XMLHttpRequest Level 2
XMLHttpRequest Level 2 针对 XMLHttpRequest Level 1 的缺点,做了大幅改进。具体如下:
- 可以设置HTTP请求的超时时间。
- 可以使用FormData对象管理表单数据。
- 可以上传文件。
- 可以请求不同域名下的数据(跨域请求)。
- 可以获取服务器端的二进制数据。
- 可以获得数据传输的进度信息
<input id="input" type="file">
var input = document.getElementById("input"),
formData = new FormData();
formData.append("file",input.files[0]); // file名称与后台接收的名称一致
var xhr = new XMLHttpRequest();
xhr.open('POST', url);
xhr.timeout = 5000; // 1
xhr.addEventListener('load', function() { ... }); // 2
xhr.addEventListener('error', function() { ... }); // 3
var onProgressHandler = function(event) {
if(event.lengthComputable) {
var progress = (event.loaded / event.total) * 100; // 4
...
}
}
xhr.upload.addEventListener('progress', onProgressHandler); // 5
xhr.addEventListener('progress', onProgressHandler); // 6
// (1) 设置请求超时时间为 5,000 ms (默认无超时时间)
// (2) 注册成功回调
// (3) 注册异常回调
// (4) 计算已完成的进度
// (5) 注册上传进度事件回调
// (6) 注册下载进度事件回调
xhr.send(formData);
轮询
定时轮询
从服务器检索更新的最简单的策略之一是让客户端进行定期检查:客户端可以以周期性间隔(轮询服务器)启动后台XHR请求,以检查更新。如果新数据在服务器上可用,则在响应中返回,否则响应为空。
长轮询
通过保持长连接,直到更新可用,数据可以立即发送到客户端,一旦它在服务器上可用。因此,长时间轮询为消息延迟提供了最佳的情况,并且还消除了空检查,这减少了 XHR 请求的数量和轮询的总体开销。一旦更新被传递,长的轮询请求完成,并且客户端可以发出另一个长轮询请求并等待下一个可用的消息:
function checkUpdates(url) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() { // 1
...
checkUpdates('/updates'); // 2
};
xhr.send();
}
checkUpdates('/updates'); // 3
// (1) 处理接收到的数据并启动下一轮检测更新
// (2) 启动下一轮检测更新
// (3) 发起首次更新请求
Ajax
基于xmlHttpRequest
参考
模仿ajax封装xmlHttpRequest
const http = {
/**
* js封装ajax请求
* >>使用new XMLHttpRequest 创建请求对象,所以不考虑低端IE浏览器(IE6及以下不支持XMLHttpRequest)
* >>使用es6语法,如果需要在正式环境使用,则可以用babel转换为es5语法 https://babeljs.cn/docs/setup/#installation
* @param settings 请求参数模仿jQuery ajax
* 调用该方法,data参数需要和请求头Content-Type对应
* Content-Type data 描述
* application/x-www-form-urlencoded 'name=哈哈&age=12'或{name:'哈哈',age:12} 查询字符串,用&分割
* application/json name=哈哈&age=12' json字符串
* multipart/form-data new FormData() FormData对象,当为FormData类型,不要手动设置Content-Type
* 注意:请求参数如果包含日期类型.是否能请求成功需要后台接口配合
*/
ajax: (settings = {}) => {
// 初始化请求参数
let _s = Object.assign({
url: '', // string
type: 'GET', // string 'GET' 'POST' 'DELETE'
dataType: 'json', // string 期望的返回数据类型:'json' 'text' 'document' ...
async: true, // boolean true:异步请求 false:同步请求 required
data: null, // any 请求参数,data需要和请求头Content-Type对应
headers: {}, // object 请求头
timeout: 1000, // string 超时时间:0表示不设置超时
beforeSend: (xhr) => {
},
success: (result, status, xhr) => {
},
error: (xhr, status, error) => {
},
complete: (xhr, status) => {
}
}, settings);
// 参数验证
if (!_s.url || !_s.type || !_s.dataType || _s.async === undefined) {
alert('参数有误');
return;
}
// 创建XMLHttpRequest请求对象
let xhr = new XMLHttpRequest();
// 请求开始回调函数
xhr.addEventListener('loadstart', e => {
_s.beforeSend(xhr);
});
// 请求成功回调函数
xhr.addEventListener('load', e => {
const status = xhr.status;
if ((status >= 200 && status < 300) || status === 304) {
let result;
if (xhr.responseType === 'text') {
result = xhr.responseText;
} else if (xhr.responseType === 'document') {
result = xhr.responseXML;
} else {
result = xhr.response;
}
// 注意:状态码200表示请求发送/接受成功,不表示业务处理成功
_s.success(result, status, xhr);
} else {
_s.error(xhr, status, e);
}
});
// 请求结束
xhr.addEventListener('loadend', e => {
_s.complete(xhr, xhr.status);
});
// 请求出错
xhr.addEventListener('error', e => {
_s.error(xhr, xhr.status, e);
});
// 请求超时
xhr.addEventListener('timeout', e => {
_s.error(xhr, 408, e);
});
let useUrlParam = false;
let sType = _s.type.toUpperCase();
// 如果是"简单"请求,则把data参数组装在url上
if (sType === 'GET' || sType === 'DELETE') {
useUrlParam = true;
_s.url += http.getUrlParam(_s.url, _s.data);
}
// 初始化请求
xhr.open(_s.type, _s.url, _s.async);
// 设置期望的返回数据类型
xhr.responseType = _s.dataType;
// 设置请求头
for (const key of Object.keys(_s.headers)) {
xhr.setRequestHeader(key, _s.headers[key]);
}
// 设置超时时间
if (_s.async && _s.timeout) {
xhr.timeout = _s.timeout;
}
// 发送请求.如果是简单请求,请求参数应为null.否则,请求参数类型需要和请求头Content-Type对应
xhr.send(useUrlParam ? null : http.getQueryData(_s.data));
},
// 把参数data转为url查询参数
getUrlParam: (url, data) => {
if (!data) {
return '';
}
let paramsStr = data instanceof Object ? http.getQueryString(data) : data;
return (url.indexOf('?') !== -1) ? paramsStr : '?' + paramsStr;
},
// 获取ajax请求参数
getQueryData: (data) => {
if (!data) {
return null;
}
if (typeof data === 'string') {
return data;
}
if (data instanceof FormData) {
return data;
}
return http.getQueryString(data);
},
// 把对象转为查询字符串
getQueryString: (data) => {
let paramsArr = [];
if (data instanceof Object) {
Object.keys(data).forEach(key => {
let val = data[key];
// todo 参数Date类型需要根据后台api酌情处理
if (val instanceof Date) {
// val = dateFormat(val, 'yyyy-MM-dd hh:mm:ss');
}
paramsArr.push(encodeURIComponent(key) + '=' + encodeURIComponent(val));
});
}
return paramsArr.join('&');
}
/**
* 根据实际业务情况装饰 ajax 方法
* 如:统一异常处理,添加http请求头,请求展示loading等
* @param settings
*/
request: (settings = {}) => {
// 统一异常处理函数
let errorHandle = (xhr, status) => {
console.log('request error...');
if (status === 401) {
console.log('request 没有权限...');
}
if (status === 408) {
console.log('request timeout');
}
};
// 使用before拦截参数的 beforeSend 回调函数
settings.beforeSend = (settings.beforeSend || function () {
}).before(xhr => {
console.log('request show loading...');
});
// 保存参数success回调函数
let successFn = settings.success;
// 覆盖参数success回调函数
settings.success = (result, status, xhr) => {
// todo 根据后台api判断是否请求成功
if (result && result instanceof Object && result.code !== 1) {
errorHandle(xhr, status);
} else {
console.log('request success');
successFn && successFn(result, status, xhr);
}
};
// 拦截参数的 error
settings.error = (settings.error || function () {
}).before((result, status, xhr) => {
errorHandle(xhr, status);
});
// 拦截参数的 complete
settings.complete = (settings.complete || function () {
}).after((xhr, status) => {
console.log('request hide loading...');
});
// 请求添加权限头,然后调用http.ajax方法
(http.ajax.before(http.addAuthorizationHeader))(settings);
},
// 添加权限请求头
addAuthorizationHeader: (settings) => {
settings.headers = settings.headers || {};
const headerKey = 'Authorization'; // todo 权限头名称
// 判断是否已经存在权限header
let hasAuthorization = Object.keys(settings.headers).some(key => {
return key === headerKey;
});
if (!hasAuthorization) {
settings.headers[headerKey] = 'test'; // todo 从缓存中获取headerKey的值
}
}
// 给http对象添加了get,post等方法,这些方法主要设置了默认参数然后调http.request
get: (url, data, successCallback, dataType = 'json') => {
http.request({
url: url,
type: 'GET',
dataType: dataType,
data: data,
success: successCallback
});
},
delete: (url, data, successCallback, dataType = 'json') => {
http.request({
url: url,
type: 'DELETE',
dataType: dataType,
data: data,
success: successCallback
});
},
// 调用此方法,参数data应为查询字符串或普通对象
post: (url, data, successCallback, dataType = 'json') => {
http.request({
url: url,
type: 'POST',
dataType: dataType,
data: data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
success: successCallback
});
},
// 调用此方法,参数data应为json字符串
postBody: (url, data, successCallback, dataType = 'json') => {
http.request({
url: url,
type: 'POST',
dataType: dataType,
data: data,
headers: {
'Content-Type': 'application/json; charset=UTF-8'
},
success: successCallback
});
}
}
Function.prototype.before = function (beforeFn) { // eslint-disable-line
let _self = this;
return function () {
beforeFn.apply(this, arguments);
_self.apply(this, arguments);
};
};
Function.prototype.after = function (afterFn) { // eslint-disable-line
let _self = this;
return function () {
_self.apply(this, arguments);
afterFn.apply(this, arguments);
};
};
axios
参考
axios 是一个基于 Promise 的 http请求库,可以用在浏览器和 node.js 中,本质上也是对原生XHR的封装,只不过它是Promise 的实现版本,符合最新的ES规则。
axios有什么特性?
- .可以用在浏览器和node.js的环境中,从浏览器中创建XMLHttpRequests;基于node内置核心模块http实现axios,从中创建http请求,也就是说,axios可以在浏览器上和服务器上都可以发起请求
- 是一个基于Promise的HTTP库,支持promise的所有API,源码中返回的是一个promise对象
- 拦截请求和响应
- 可以转换请求数据和响应数据,并对响应的内容自动转换为json类型的数据
- 安全性更高,客户端支持防御XSRF
- 首先axios会判断当前环境是否为标准的浏览器环境,如果是标准的浏览器环境的话,就会继续执行。
- 对于XSRF攻击来说,最基本的就是跨域了,所以我们对发出请求的域和当前的域做同源判断,如果是跨域的话,就必须有凭证config.withCredentials || isURLSameOrigin(fullPath),凭证简单来说就是当前请求在跨域时是否可以带上cookie。
- 取消请求
axios提供了取消请求的接口,实际上在源码中还是通过最关键的一步request.abort()来取消请求,使用isCancel来作为是否已经取消的标识,使用Cancel来作为重复取消时抛出的错误,使用CancelToken来作为取消请求的核心处理,在处理中采取promise异步的方法
实现axios
- 基于XMLHttpRequest实现request方法(代码里的sendAjax),通过Promise,resolve(xhr.responseText)。
- 将request方法绑定到axios对象,通过bind(即最终返回的是axios.request对象)。
- 将get、post、put等方法挂载到axios.request原型上(
prototype[method]
),这里先将其挂载到Axios原型,再挂载到Axios.request原型上。 - 添加request和response拦截器,改写request方法,将request方法变为方法对象数组
[{onFullFilled, onRejected}]
,先添加[sendAjax, undefined]
,然后将request回调依次插入队首,response回调依次插入队尾,最后通过then方法返回一个Promise(r => { resolve(data); })
class InterceptorsManage {
constructor() {
this.handlers = [];
}
use(fullfield, rejected) {
this.handlers.push({
fullfield,
rejected
})
}
}
class Axios {
constructor() {
this.interceptors = {
request: new InterceptorsManage,
response: new InterceptorsManage
}
}
request(config) {
// 拦截器和请求组装队列
let chain = [this.sendAjax.bind(this), undefined] // 成对出现的,失败回调暂时不处理
// 请求拦截
this.interceptors.request.handlers.forEach(interceptor => {
chain.unshift(interceptor.fullfield, interceptor.rejected)
})
// 响应拦截
this.interceptors.response.handlers.forEach(interceptor => {
chain.push(interceptor.fullfield, interceptor.rejected)
})
// 执行队列,每次执行一对,并给promise赋最新的值
let promise = Promise.resolve(config);
while(chain.length > 0) {
promise = promise.then(chain.shift(), chain.shift())
}
return promise;
}
sendAjax(){
return new Promise(resolve => {
const {url = '', method = 'get', data = {}} = config;
// 发送ajax请求
console.log(config);
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.onload = function() {
console.log(xhr.responseText)
resolve(xhr.responseText);
};
xhr.send(data);
})
}
}
// 定义get,post...方法,挂在到Axios原型上
const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post'];
methodsArr.forEach(met => {
Axios.prototype[met] = function() {
console.log('执行'+met+'方法');
// 处理单个方法
if (['get', 'delete', 'head', 'options'].includes(met)) { // 2个参数(url[, config])
return this.request({
method: met,
url: arguments[0],
...arguments[1] || {}
})
} else { // 3个参数(url[,data[,config]])
return this.request({
method: met,
url: arguments[0],
data: arguments[1] || {},
...arguments[2] || {}
})
}
}
})
// 工具方法,实现b的方法混入a;
// 方法也要混入进去
const utils = {
extend(a,b, context) {
for(let key in b) {
if (b.hasOwnProperty(key)) {
if (typeof b[key] === 'function') {
a[key] = b[key].bind(context);
} else {
a[key] = b[key]
}
}
}
}
}
// 最终导出axios的方法-》即实例的request方法
function CreateAxiosFn() {
let axios = new Axios();
let req = axios.request.bind(axios);
// 混入方法, 处理axios的request方法,使之拥有get,post...方法
utils.extend(req, Axios.prototype, axios)
return req;
}
// 得到最后的全局变量axios
let axios = CreateAxiosFn();