XMLHttpRequest (XHR)对象是一个基于浏览器层面的API,可以通过JavaScript 实 现 数 据 传 输。 为后来的AJAX提供了革命性的技术支持;
主要 用于与服务器交互
XMLHttpRequest 文章中统一用简称(XHR)代替
主要讨论
XHR 怎么使用?
通过js 怎么实现数据传输?
既然是基于浏览器的API,浏览器为我们做了那些事情?
传输过程的安全机制是怎么产生的?
跨域(CORS)是怎么产生的?如何访问跨域?
XHR 对象的使用和普通的对象一毛一样,也是需要new的,然后在调用对象里面的方法。但是又有自己的小心思:
let xhr = new XMLHttpRequest();
xhr.open('get','/example.do',false);
这里调用了open 方法,但是并不会向服务器发送请求,可以简单理解为启动一个备发送
这个就和普通对象不一样了。还需要send()方法组合才能真正的发送到服务器
open 语法
两个必传 三个可选
xhr.open(method, url, async, user, password);
method | 必传 要使用的HTTP方法,比如「GET」、「POST」、「PUT」、「DELETE」、等 |
---|---|
url | 必传 发送请求的URL (相对 |
async | 可选 默认值为true 是否是异步请求 |
user | 可选 默认值为null 用户名用于认证用途 |
password | 可选 默认值为null 密码用于认证用途 |
通常在项目中我们只是关心前面两个参数或者三个参数
要使xhr.open() 成功请求到服务器,还需要send方法,以上案例要请求成功还需要加上如下操作
xhr.send(null);
send 方法接受一个可选的参数,其作为请求主体;如果请求方法是 GET 或者 HEAD,则应将请求主体设置为 null 如上面的 方法是get,如果是post 就传入相关的数据
向服务器发送请求的目的是期待获取到数据为我所用,那么数据返回到哪里了?
关于数据去哪里了。首先需要介绍XHR的两个属性嘉宾,readyState 和status
readyState 用来标识当前XMLHttpRequest对象处于什么状态。 这些状态值记录者XHR当前的情况,一共有5中状态来描述
readyState 状态值 | 描述 |
---|---|
0 | 此时,已经创建了一个XMLHttpRequest对象 |
1 | 此时,已经调用了XMLHttpRequest对象的open方法 |
2 | 此时,已经通过send方法把一个请求发送到服务器端,但是还没有收到一个响应 |
3 | 此时,已经接收到HTTP响应头部信息,但是消息体部分还没有完全接收到 |
4 | 此时,已经完成了HTTP响应的接收 |
status 是XMLHttpRequest对象的一个属性,但是它表示响应的HTTP状态码 无论是否请求成功,服务器都会返回的HTTP头信息代码,有5大类的状态码返回
status 类 | 描述 |
---|---|
1xx | 服务器收到请求,需要继续处理 |
2xx | 处理成功响应类,表示动作被成功接收、理解和接受 |
3xx | 重定向响应类,为了完成指定的动作,必须接受进一步处理 |
4xx | 客户端错误,客户请求包含语法错误或者是不能正确执行 |
5xx | 服务端错误,服务器不能正确执行一个正确的请求 |
几个常用的状态码
status状态码 | |
---|---|
200 | 请求成功 表示请求所希望的响应头或数据体将随此响应返回 |
302 | 表示临时重定向,请求将包含一个新的URL地址,客户端将对新的地址进行GET请求 |
304 | 标示请求的资源没有修改,可以直接用浏览器的缓存数据 |
404 | 表示客户端请求的资源不存在 |
500 | 表示服务器遇到了一个未曾预料的情况,导致了它无法完成响应 服务器产生内部错误 |
而 readyState 的状态值每变化一次都会触发一个XHR对象的事件onreadystatechange,从而更友好的获取到想要到数据
总的来说只要服务器接受到请求,就会自动去填充XHR相关属性,作为js到使用者接受即可
填充的相关XHR属性 | 说明 |
---|---|
responseText | 作为响应主体被返回的文本,如果请求未成功或尚未发送,则返回 null。 |
responseXML | XML 形式的响应数据,如果请求未成功或尚未发送,则返回 null |
其他的不列举
为什么可以直接获取到数据还要去了解 readyState 和status 以及onreadystatechange 方法呢?
因为我们只关心对开发有用的数据 比如readyState 通常情况我们只用去关心 4这个状态值,status 关心200 和304 就可以。在来通过onreadystatechange 获取最终的的数据就是,我们需要的数据,过滤去其他没用的,这样一套完整的请求就出来了。
let xhr = new XMLHttpRequest();
xhr.open('get','/example.do',true);
// 监听readyState 的值
xhr.onreadystatechange=function(){
// xhr 的状态值
if(xhr.readyState === 4){
// http 返回状态码
if(xhr.status === 200 || xhr.status === 304){
// 等到我们期待的文本结果
console.log(xhr.responseText);
}
}
}
xhr.send(null);
已经请求成功还需要讨论剩下的吗?
如果只想手写一个ajax请求那么不需要讨论了。如果继续深入还的要继续。。。
看了xhr到用法,写起来好像并不那么费事,现在需要考虑另外到问题。是不是可以用XHR对象请求任何网站到地址,是不是可以把数据通过post 提交到任何一个网站,显然是不可能到,不然就可以不断到往我到银行卡里面存钱了对不。
为什么我们发送一个请求,服务器就会给数据,给出相关到状态,而且我只是发出一个链接。原因往下寻找。。
XHR 是一个浏览器层面到API,因此浏览器隐藏或者说帮忙处理了很多底层逻辑如:
这样做到目的一个是大大减少使用XHR的开发工作,做到简单易用,另外就是沙箱机制无形中增加了安全机制
XHR 每次发出请求,都会携带HTTP头部请求信息和接受响应之后到HTTP头部信息,同时对于每个请求都带有严格的HTTP语义:应用提供数据和 URL, 浏览 器格式化请求并管理每个连接的完整生命周期;
很多时候不需要去关心头部信息,默认情况下每次发送请求浏览器会自动加上HTTP头部信息如下:
HTTP常用头部字段 | 说明 |
---|---|
Accept | 浏览器能够处理的内容类型 如:application/json, text/javascript, /; q=0.01 |
Accept-Encoding | 处理的压缩编码 如:gzip, deflate |
Accept-Language | 当前设置的语言 如:zh-CN,zh;q=0.9 |
Connection | 与服务器的链接类型 如:keep-alive |
Cookie | 当前页面设置的所有cookie |
Host | 指定访问的域名地址 |
Referer | 发送请求页面URL 居然这是一个写错的的单词。目前只能继续错下去:referrer |
User-Agent | 浏览器表明自己的身份(是哪种浏览器) |
Origin | 请求来自于哪个站点。该字段仅指示服务器名称,并不包含任何路径信息 |
每个浏览器实际发送的头部信息会有所不同,但是上面这些基本的都会发送。
XHR API 准许通过setRequestHeader() 方法设置和修改头部信息,但是有些属性浏览器是不允许修改,那些影响首部安全的属性拒绝修改。用来保证不能假扮用户代理、用户或请求 来源。这个就是稍后要说的‘同源策略’,为了安全和操作的可靠性出发,当我们需要通过头部传递数据给服务端,推荐方法:添加自定义的方式,最高效率而且安全,比如我们需要传递一个token 个服务端校验:
let xhr = new XMLHttpRequest();
xhr.onreadystatechange={}
xhr.open('get','/example.do',true);
xhr.setRequestHeader('token',"tokenvalue");
xhr.send(null);
这样在发送请求的时候就会在HTTP头部加上这个字段,当服务器收到token,就可以作出相关当处理,然后返回给客户端;
默认情况下XHR对象只能访问与包含它的页面同一个域的资源,而保护这个源(Origin)首部就变得很重要,这个就是‘同源策略’;
一个“源”由应用协议、域名和端口这三个要件共同定义。 比如, (http, example.com, 80) 和 (https, example.com, 443) 就是不同的源。
同源策略:出发很简单,浏览器存储着用户数据, 比如认证令牌、cookie 及其 他私有元数据, 这些数据不能泄露给其他应用。 如果没有同源沙箱, 那么 example. com 中的脚本就可以访问并操纵 bank
.com 的用户数据!就可以往银行存入钱了。
// 同源请求: (http www.example.com 80)
let xhr = new XMLHttpRequest();
xhr.onreadystatechange={}
xhr.open('get','/example.do',true);
xhr.setRequestHeader('token',"tokenvalue");
xhr.send(null);
// 不同源请求
let xhr = new XMLHttpRequest();
xhr.onreadystatechange={}
xhr.open('get','https://www.baidu.com/example.do',true);
xhr.setRequestHeader('token',"tokenvalue");
xhr.send(null);
上面的同源和非同源唯一的区别就是URL地址不一样,底层的逻辑处理是这样子的:
请求发出后,浏览器自动追加受保 护的 Origin HTTP 首部,包含着发出请求的来源(协议、域名和端口)。相应地,远程服务器可以检查 Origin 首部,决定是否接受该请求,如果接受就返回 Access-Control-Allow-Origin 响应首部:
// 没有跨域
=> 请求 GET /example.do HTTP/1.1
Host: example.com
Origin: www.example.com
...
<= 响应 HTTP/1.1 200 OK
Access-Control-Allow-Origin: http: www.example.com
...
很多情况下,还是需要给另外一个网站的脚本提供资源。这个时候就发生如上例子的情况,不是同源请求了。非同源请求也叫跨域资源请求,那么怎么实现请求呢?
实现的机制就是 Cross-Origin Resource Sharing(跨源资源共享,CORS)! CORS 针对客户端的跨源请求提供了安 全的选择同意机制;
用代码来解释:
// 跨域请求
=> 请求 GET /example.do HTTP/1.1
Host: baidu.com
Origin: www.example.com
...
<= 响应 HTTP/1.1 200 OK
Access-Control-Allow-Origin: http: www.example.com
...
上面的代码baidu.com 决定是否同意http: www.example.com 跨域资源共享,因此就在返回的首部返回中加入Access-Control-Allow-Origin 首部即可,如果服务器不同意请求 就不返回Access-Control-Allow-Origin ,客户端浏览器就会自动作废请求;
如果第三方服务器不支持 CORS, 那么客户端请求同样会作废, 因为客户 端会验证响应中是否包含选择同意的首部。 作为一个特例, CORS 还允许 服务器返回一个通配值 ( Access-Control-Allow-Origin: * ), 表示它允许 来自任何源的请求。不过实际很少准许所有的域去请求
XHR 既可以传输文本数据 如上面的例子, 也可以传输二进制数据,而且浏览器会自动提供转码和解码服务
自动解码的数据类型 | 说明 |
---|---|
ArrayBuffer | 固定长度的二进制数据缓冲区 |
Blob | 二进制大对象或不可变数据 |
Document | 解析后得到的HTML或XML文档。 |
JSON | 简单数据结构的js对象 |
Text | 简单的文本字符串 |
浏 览 器 可 以 依 靠 HTTP 的 content-type 首 部 来 推 断 适 当 的 数 据 类 型( 比 如 把 application/json 响应解析为 JSON 对象),应用可以修改这个头部请求而且可以更改显示数据类型:
let xhr = new XMLHttpRequest();
xhr.open('get','./example.webp');
xhr.responseType='Blod';
xhr.onload=function(){
if(xhr.status === 200){
let img = document.createElement('img');
img.src = window.URL.createObjectURL(xhr.response);
img.onload=function(){
window.URL.revokeObjectURL(img.src);
}
document.body.appendChild(img);
}
}
xhr.send(null);
这里我们在以原生格式传输一张图片, 没有使用 base64 编码, 也没有使用数 据 URI, 而是在页面中添加了一个 元素。 这样在 JavaScript 中处理接收到的 二进制数据不会产生任何网络传输开销和编码开销! XHR API 让我们得以通过 脚本高效、动态地开发应用, 无论操作什么数据类型都没问题, 全部用 JavaScript 搞定!JSON 原理一样
onload 事件改造之后可以代替 onreadystatechange 从而可以不用去监控readyState 状态,封装得到更大的提升
对于xhr 上传任何数据都很简单,而且高效,对于不同的数据类型只需在xhr对象send的方法上传入数据对象即可,其他代码都一样,剩下的浏览器会处理
let fromData = new fromData();
fromData.append('id','0001');
fromData.append('name','木子聊前端');
let xhr = new XMLHttpRequest();
xhr.onload = function(){}
xhr.open('POST','/upload.do');
// let fromData = new fromData();
fromData.append('id','0001');
fromData.append('name','木子聊前端');
let xhr = new XMLHttpRequest();
xhr.onload = function(){}
xhr.open('POST','/upload.do');
// 向服务器上传 multipart/form-data 对象 new fromData 是一个表单对象
xhr.send(fromData);
XHR对 象 的 send() 方 法 可 以 接 受 DOMString 、 Document 、 FormData 、 Blob 、 File 及 ArrayBuffer 对象,并自动完成相应的编码,设置适当的 HTTP 内容类型 ( content-type ), 然后再分派请求。 需要发送二进制 Blob 或上传用户提交的文件?简单, 取得对该对 象的引用,传给 XHR。 事实上,多写几行代码,还可以把大文件切成几小块:
let blod = '非常大大二进制数据';
// 将上传大块设置为1M
const chunk_size = 1024*1024;
// 获取文件流的总大小
const size = blod.size;
// 设置每次传输到范围
let start = 0;
let end = chunk_size;
while(start<size){
let xhr = new XMLHttpRequest();
xhr.onload =(){...}
// 告诉服务器发送范围
xhr.setRequestHeader('Content-Range',start+'-'+end+'/'+size);
xhr.open('POST','/upload.do');
// 通过 XHR 上传 1 MB 大小的数据片段
xhr.send(blob.slice(start, end));
start = end;
end = start + chunk_size;
}
XHR 不支持请求流, 这意味着在调用 send() 时必须提供完整的文件。 不过, 前面 的例子示范了一个简单的解决方案:切分文件, 然后通过多个 XHR 请求分段上传。
网络连接可能会间歇性中断, 而延迟和带宽也高度不稳定。 因此, 我们怎么知道 XHR 请求成功了, 超时了, 还是失败了? XHR 对象提供了一个方便的 API, 用于 监控进度事件如下表格 ,这些事件代表请求的当前状态。。
事件类型 | 说明 | 触发次数 |
---|---|---|
loadstart | 传输已开始 | 一次 |
progress | 正在传输 | 零或多次 |
error | 传输出错 | 零或多次 |
abort | 传输终止 | 零或多次 |
load | 传输成功 | 零或多次 |
loadend | 传输完成 | 一次 |
var xhr = new XMLHttpRequest();
xhr.open('GET','/resource');
// 设置请求的超时时间为 5000 ms(默认无超时限制)
xhr.timeout = 5000;
// 为请求成功注册回调
xhr.addEventListener('load', function() { ... });
// 为请求失败注册回调
xhr.addEventListener('error', function() { ... });
// 计算传输进度
var onProgressHandler = function(event) {
if(event.lengthComputable) { var progress = (event.loaded / event.total) * 100;
...
}
}
// 为上传进度事件注册回调
xhr.upload.addEventListener('progress', onProgressHandler);
// 为下载进度事件注册回调
xhr.addEventListener('progress', onProgressHandler);
xhr.send();
无论 load 和 error 中 的 哪 一 个 被 触 发 了, 都 代 表 XHR 传 输 的 最 终 状 态, 而 progress 事件则可能触发任意多次, 这就为监控传输状态提供了便利:我们可以比 较 loaded 与 total 属性,估算传输完成的数据比例。
要估算传输完成的数据量,服务器必须在其响应中提供内容长度(ContentLength)首部。 而对于分块数据,由于响应的总长度未知,因此就无法估计 进度了。 另外, XHR 请求默认没有超时限制,这意味着一个请求的“进度”可以无限 长。作为最佳实践,一定要为应用设置合理的超时时间,并适当处理错误。
喜欢请点击收藏,谢谢