ajax核心技术 XMLHttpRequest 对象 性能安全 探讨和理解

内容提要

XMLHttpRequest (XHR)对象是一个基于浏览器层面的API,可以通过JavaScript 实 现 数 据 传 输。 为后来的AJAX提供了革命性的技术支持;
主要 用于与服务器交互

XMLHttpRequest 文章中统一用简称(XHR)代替
主要讨论
XHR 怎么使用?
通过js 怎么实现数据传输?
既然是基于浏览器的API,浏览器为我们做了那些事情?
传输过程的安全机制是怎么产生的?
跨域(CORS)是怎么产生的?如何访问跨域?

XHR 用法

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请求那么不需要讨论了。如果继续深入还的要继续。。。

同源策略和跨源资源共享(CORS)

看了xhr到用法,写起来好像并不那么费事,现在需要考虑另外到问题。是不是可以用XHR对象请求任何网站到地址,是不是可以把数据通过post 提交到任何一个网站,显然是不可能到,不然就可以不断到往我到银行卡里面存钱了对不。
为什么我们发送一个请求,服务器就会给数据,给出相关到状态,而且我只是发出一个链接。原因往下寻找。。

XHR 是一个浏览器层面到API,因此浏览器隐藏或者说帮忙处理了很多底层逻辑如:

  • 管理着连接建立、套接字池和连接终止;
  • 决定最佳的 HTTP(S)传输协议
  • 处理 HTTP 缓存、重定向和内容类型协商
  • 保障安全、验证和隐私
  • … …

这样做到目的一个是大大减少使用XHR的开发工作,做到简单易用,另外就是沙箱机制无形中增加了安全机制

请求HTTP头部信息

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 数据下载

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 上传任何数据都很简单,而且高效,对于不同的数据类型只需在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 请求默认没有超时限制,这意味着一个请求的“进度”可以无限 长。作为最佳实践,一定要为应用设置合理的超时时间,并适当处理错误。

喜欢请点击收藏,谢谢

你可能感兴趣的:(javascript,javascript)