你不知道的 XMLHttpRequest

本文详细介绍了 XMLHttpRequest 相关知识,涉及内容:javascript

  • AJAX、XMLHTTP、XMLHttpRequest详解、XMLHttpRequest Level 一、Level 2 详解php

  • XHR 上传、下载数据、XHR 流式传输、XHR 定时轮询和长轮询区别与优缺点、XMLHttpRequest 库 (Mock.js、Zone.js、Oboe.js、fetch.js)html

  • XMLHttpRequest 经常使用代码片断:前端

    • ArrayBuffer 对象转字符串html5

    • 字符串转 ArrayBuffer 对象java

    • 建立 XHR 对象node

    • sendAsBinary() polyfilljquery

    • 获取 XMLHttpRequest 响应体ios

    • 获取 responseURLgit

    • 验证请求是否成功

    • 解析查询参数为Map对象

    • XHR 下载图片

    • XHR 上传图片

    • XHR 上传进度条

  • 分析 AJAX 请求状态为 0、GET请求方式为何不能经过send() 方法发送请求体、简单请求和预请求、XMLHttpRequest对象垃圾回收机制、Get与Post请求区别、如何避免重复发送请求、AJAX 站点 SEO 优化等问题。

AJAX

AJAX 定义

AJAX即“Asynchronous JavaScript and XML”(异步的JavaScriptXML技术),指的是一套综合了多项技术的浏览器网页开发技术。Ajax的概念由杰西·詹姆士·贾瑞特所提出。

传统的Web应用容许用户端填写表单(form),当提交表单时就向网页服务器发送一个请求。服务器接收并处理传来的表单,而后送回一个新的网页,但这个作法浪费了许多带宽,由于在先后两个页面中的大部分HTML码每每是相同的。因为每次应用的沟通都须要向服务器发送请求,应用的回应时间依赖于服务器的回应时间。这致使了用户界面的回应比本机应用慢得多。

与此不一样,AJAX应用能够仅向服务器发送并取回必须的数据,并在客户端采用JavaScript处理来自服务器的回应。由于在服务器和浏览器之间交换的数据大量减小(大约只有原来的5%)[来源请求],服务器回应更快了。同时,不少的处理工做能够在发出请求的客户端机器上完成,所以Web服务器的负荷也减小了。

相似于DHTMLLAMP,AJAX不是指一种单一的技术,而是有机地利用了一系列相关的技术。虽然其名称包含XML,但实际上数据格式能够由JSON代替,进一步减小数据量,造成所谓的AJAJ。而客户端与服务器也并不须要异步。一些基于AJAX的“派生/合成”式(derivative/composite)的技术也正在出现,如AFLAX。 —— 维基百科

AJAX 应用

AJAX 兼容性

JavaScript 编程的最大问题来自不一样的浏览器对各类技术和标准的支持。

XmlHttpRequest 对象在不一样浏览器中不一样的建立方法,如下是跨浏览器的通用方法:

// Provide the XMLHttpRequest class for IE 5.x-6.x:
// Other browsers (including IE 7.x-8.x) ignore this
//   when XMLHttpRequest is predefined
var xmlHttp;
if (typeof XMLHttpRequest != "undefined") {
    xmlHttp = new XMLHttpRequest();
} else if (window.ActiveXObject) {
    var aVersions = ["Msxml2.XMLHttp.5.0", "Msxml2.XMLHttp.4.0", 
        "Msxml2.XMLHttp.3.0", "Msxml2.XMLHttp", "Microsoft.XMLHttp"];
    for (var i = 0; i < aVersions.length; i++) {
        try {
            xmlHttp = new ActiveXObject(aVersions[i]);
            break;
        } catch (e) {}
    }
}

详细信息请参考 - Can I use XMLHttpRequest

AJAX/HTTP 库对比

Support Features
All Browsers Chrome & Firefox1 Node Concise Syntax Promises Native2 Single Purpose3 Formal Specification
XMLHttpRequest
Node HTTP
fetch()
Fetch polyfill
node-fetch
isomorphic-fetch
superagent
axios
request
jQuery
reqwest

1 Chrome & Firefox are listed separately because they support fetch(): caniuse.com/fetch
2 Native: Meaning you can just use it - no need to include a library.
3 Single Purpose: Meaning this library or technology is ONLY used for AJAX / HTTP communication, nothing else.

详细信息请参考 - AJAX/HTTP Library Comparison

XMLHTTP

XMLHTTP 定义

XMLHTTP 是一组API函数集,可被JavaScript、JScript、VBScript以及其它web浏览器内嵌的脚本语言调用,经过HTTP在浏览器和web服务器之间收发XML或其它数据。XMLHTTP最大的好处在于能够动态地更新网页,它无需从新从服务器读取整个网页,也不须要安装额外的插件。该技术被许多网站使用,以实现快速响应的动态网页应用。例如:GoogleGmail服务、Google Suggest动态查找界面以及Google Map地理信息服务。

XMLHTTP是AJAX网页开发技术的重要组成部分。除XML以外,XMLHTTP还能用于获取其它格式的数据,如JSON或者甚至纯文本。—— 维基百科

XMLHTTP 背景知识

XMLHTTP最初是由微软公司发明的,在Internet Explorer 5.0中用做ActiveX对象,可经过JavaScript、VBScript或其它浏览器支持的脚本语言访问。Mozilla的开发人员后来在Mozilla 1.0中实现了一个兼容的版本。以后苹果电脑公司在Safari 1.2中开始支持XMLHTTP,而Opera从8.0版开始也宣布支持XMLHTTP。

大多数使用了XMLHTTP的设计良好的网页,会使用简单的JavaScript函数,将不一样浏览器之间调用XMLHTTP的差别性屏蔽,该函数会自动检测浏览器版本并隐藏不一样环境的差别。

DOM 3(文档对象模型 Level 3)的读取和保存规范(Load and Save Specification)中也有相似的功能,它已经成为W3C推荐的方法。截止2011年,大多数浏览器已经支持。—— 维基百科

XMLHTTP 实现

  • ActiveXObject

  • XMLHttpRequest

什么是 ActiveX 控件

Microsoft ActiveX 控件是由软件提供商开发的可重用的软件组件。使用 ActiveX 控件,能够很快地在网址、台式应用程序、以及开发工具中加入特殊的功能。例如,StockTicker 控件能够用来在网页上即时地加入活动信息,动画控件可用来向网页中加入动画特性。

ActiveXObject 对象

JavaScript 中 ActiveXObject 对象是启用并返回 Automation 对象的引用。

ActiveXObject 语法

newObj = new ActiveXObject(servername.typename[, location])

参数:

  • newObj

    • 必选 - ActiveXObject 分配到的变量名称

  • servername

    • 必选 - 提供对象的应用程序名称

  • typename

    • 必选 - 要建立的对象的类型或类

  • location

    • 可选 - 要再其中建立对象的网络服务器的名称

ActiveXObject 使用

// 在IE5.x和IE6下建立xmlHttp对象
// servername - MSXML2
// typename - XMLHTTP.3.0
var xmlHttp = new ActiveXObject('MSXML2.XMLHTTP.3.0');
xmlHttp.open("GET", "http://localhost/books.xml", false);  
xmlHttp.send();

详细信息能够参考 - msdn - JavaScript 对象 - ActiveXObject 对象.aspx)

XMLHttpRequest

XMLHttpRequest 是一个API, 它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个经过 URL 来获取数据的简单方式,而且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。

XMLHttpRequest 是一个 JavaScript 对象,它最初由微软设计,随后被 Mozilla、Apple 和 Google采纳. 现在,该对象已经被 W3C组织标准化. 经过它,你能够很容易的取回一个URL上的资源数据. 尽管名字里有XML, 但 XMLHttpRequest 能够取回全部类型的数据资源,并不局限于XML。 并且除了HTTP ,它还支持file 和 ftp 协议。

XMLHttpRequest 语法

var req = new XMLHttpRequest();

XMLHttpRequest 使用

var xhr = new XMLHttpRequest(); // 建立xhr对象
xhr.open( method, url );
xhr.onreadystatechange = function () { ... };
xhr.setRequestHeader( ..., ... );
xhr.send( optionalEncodedData );

XMLHttpRequest 详解

构造函数

用于初始化一个 XMLHttpRequest 对象,必须在全部其它方法被调用前调用构造函数。使用示例以下:

var req = new XMLHttpRequest();

属性

  • onreadystatechange: Function - 当 readyState 属性改变时会调用它。

  • readyState: unsigned short - 用于表示请求的五种状态:

状态 描述
0 UNSENT (未打开) 表示已建立 XHR 对象,open() 方法还未被调用
1 OPENED (未发送) open() 方法已被成功调用,send() 方法还未被调用
2 HEADERS_RECEIVED (已获取响应头) send() 方法已经被调用,响应头和响应状态已经返回
3 LOADING (正在下载响应体) 响应体下载中,responseText中已经获取了部分数据
4 DONE (请求完成) 整个请求过程已经完毕
  • response: varies - 响应体的类型由 responseType 来指定,能够是 ArrayBuffer、Blob、Document、JSON,或者是字符串。若是请求未完成或失败,则该值为 null。

  • response: varies - 响应体的类型由 responseType 来指定,能够是 ArrayBuffer、Blob、Document、JSON,或者是字符串。若是请求未完成或失败,则该值为 null。

  • responseText: DOMString - 此请求的响应为文本,或者当请求未成功或仍是未发送时未 null (只读)

  • responseType: XMLHttpRequestResponseType - 设置该值可以改变响应类型,就是告诉服务器你指望的响应格式:

响应数据类型
"" (空字符串) 字符串(默认值)
"arraybuffer" ArrayBuffer
"blob" Blob
"document" Document
"json" JSON
"text" 字符串
  • xhr.spec 规范中定义的 XMLHttpRequestResponseType 类型以下:

    enum XMLHttpRequestResponseType {
      "",
      "arraybuffer",
      "blob",
      "document",
      "json",
      "text"
    };
  • responseXML: Document - 本次请求响应式一个 Document 对象,若是是如下状况则值为 null:

    • 请求未成功

    • 请求未发送

    • 响应没法被解析成 XML 或 HTML

  • status: unsigned short - 请求的响应状态码,如 200 (表示一个成功的请求)。 (只读)

  • statusText: DOMString - 请求的响应状态信息,包含一个状态码和消息文本,如 "200 OK"。 (只读)

  • timeout: unsigned long - 表示一个请求在被自动终止前所消耗的毫秒数。默认值为 0,意味着没有超时时间。超时并不能应用在同步请求中,不然会抛出一个 InvalidAccessError 异常。当发生超时时,timeout 事件将会被触发。

  • upload: XMLHttpRequestUpload - 能够在 upload 上添加一个事件监听来跟踪上传过程

  • withCredentials: boolean - 代表在进行跨站 (cross-site) 的访问控制 (Access-Control) 请求时,是否使用认证信息 (例如cookie或受权的header)。默认为 false。注意:这不会影响同站 same-site 请求

方法

  • abort() - 若是请求已经被发送,则马上停止请求。

  • getAllResponseHeaders() - 返回全部响应头信息(响应头名和值),若是响应头尚未接收,则返回 null。注意:使用该方法获取的 response headers 与在开发者工具 Network 面板中看到的响应头不一致

  • getResponseHeader() - 返回指定响应头的值,若是响应头尚未被接收,或该响应头不存在,则返回 null。注意:使用该方法获取某些响应头时,浏览器会抛出异常,具体缘由以下:

    和 Access-Control-Expose-Headers。

  • open() - 初始化一个请求:

    • 方法签名:

      void open(
         DOMString method,
         DOMString url,
         optional boolean async,
         optional DOMString user,
         optional DOMString password
      );
    • 参数:

      • method - 请求所使用的 HTTP 方法,如 GET、POST、PUT、DELETE

      • url - 请求的 URL 地址

      • async - 一个可选的布尔值参数,默认值为 true,表示执行异步操做。若是值为 false,则 send() 方法不会返回任何东西,直到接收到了服务器的返回数据

      • user - 用户名,可选参数,用于受权。默认参数为空字符串

      • password - 密码,可选参数,用于受权。默认参数为空字符串

    • 备注:

      • 若是 method 不是有效的 HTTP 方法或 url 地址不能被成功解析,将会抛出 SyntaxError 异常

      • 若是请求方法(不区分大小写)为 CONNECTTRACETRACK 将会抛出 SecurityError 异常

  • overrideMimeType() - 重写由服务器返回的 MIME 类型。例如,能够用于强制把响应流当作 text/xml 来解析,即便服务器没有指明数据是这个类型。注意:这个方法必须在 send() 以前被调用。

  • send() - 发送请求。若是该请求是异步模式(默认),该方法会马上返回。相反,若是请求是同步模式,则直到请求的响应彻底接受之后,该方法才会返回。注意:全部相关的事件绑定必须在调用 send() 方法以前进行。

    • 方法签名:

      void send();
      void send(ArrayBuffer data);
      void send(Blob data);
      void send(Document data);
      void send(DOMString? data);
      void send(FormData data);
  • setRequestHeader() - 设置 HTTP 请求头信息。注意:在这以前,你必须确认已经调用了 open() 方法打开了一个 url

    • 方法签名:

      void setRequestHeader(
         DOMString header,
         DOMString value
      );
    • 参数:

      • header - 请求头名称

      • value - 请求头的值

  • sendAsBinary() - 发送二进制的 send() 方法的变种。

    • 方法签名:

      void sendAsBinary(
         in DOMString body
      );
    • 参数:

      • body - 消息体

浏览器兼容性

  • Desktop

Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari (WebKit)
Basic support (XHR1) 1 1.0 5 (via ActiveXObject)7 (XMLHttpRequest) (Yes) 1.2
send(ArrayBuffer) 9 9 ? 11.60 ?
send(Blob) 7 3.6 ? 12 ?
send(FormData) 6 4 ? 12 ?
response 10 6 10 11.60 ?
responseType = 'arraybuffer' 10 6 10 11.60 ?
responseType = 'blob' 19 6 10 12 ?
responseType = 'document' 18 11 未实现 未实现 未实现
responseType = 'json' 未实现 10 未实现 12 未实现
Progress Events 7 3.5 10 12 ?
withCredentials 3 3.5 10 12 4

事件

  • loadstart - 当程序开始加载时,loadstart 事件将被触发。

  • progress - 进度事件会被触发用来指示一个操做正在进行中。

  • abort - 当一个资源的加载已停止时,将触发 abort 事件。

  • error - 当一个资源加载失败时会触发error事件。

  • load - 当一个资源及其依赖资源已完成加载时,将触发load事件。

  • timeout - 当进度因为预约时间到期而终止时,会触发timeout 事件。

  • loadend - 当一个资源加载进度中止时 (例如,在已经分派“错误”,“停止”或“加载”以后),触发loadend事件。

  • readystatechange - readystatechange 事件会在 document.readyState属性发生变化时触发。

XMLHttpRequest Level 1

XMLHttpRequest Level 1 使用

首先,建立一个 XMLHttpRequest 对象:

var xhr = new XMLHttpRequest();

而后,向服务器发出一个 HTTP 请求:

xhr.open('GET', 'example.php');
xhr.send();

接着,就等待远程主机作出回应。这时须要监控XMLHttpRequest对象的状态变化,指定回调函数。

xhr.onreadystatechange = function(){
  if ( xhr.readyState == 4 && xhr.status == 200 ) {
     alert( xhr.responseText );
  } else {
     alert( xhr.statusText );
  }
};

上面的代码包含了老版本 XMLHttpRequest 对象的主要属性:

  • xhr.readyState: XMLHttpRequest对象的状态,等于4表示数据已经接收完毕。

  • xhr.status:服务器返回的状态码,等于200表示一切正常。

  • xhr.responseText:服务器返回的文本数据。

  • xhr.statusText:服务器返回的状态文本。

XMLHttpRequest Level 1 缺点

  • 只支持文本数据的传送,没法用来读取和上传二进制文件。

  • 传送和接收数据时,没有进度信息,只能提示有没有完成。

  • 受到"同域限制"(Same Origin Policy),只能向同一域名的服务器请求数据。

XMLHttpRequest Level 2

XMLHttpRequest Level 2 针对 XMLHttpRequest Level 1 的缺点,作了大幅改进。具体以下:

  • 能够设置HTTP请求的超时时间。

  • 可使用FormData对象管理表单数据。

  • 能够上传文件。

  • 能够请求不一样域名下的数据(跨域请求)。

  • 能够获取服务器端的二进制数据。

  • 能够得到数据传输的进度信息。

设置超时时间

新版本 XMLHttpRequest 对象,增长了 timeout 属性,能够设置HTTP请求的时限。

 xhr.timeout = 3000;

上面的语句,将最长等待时间设为3000毫秒。过了这个时限,就自动中止HTTP请求。与之配套的还有一个timeout事件,用来指定回调函数。

xhr.ontimeout = function(event){
  console.log('请求超时');
}

FormData 对象

AJAX 操做每每用来传递表单数据。为了方便表单处理,HTML 5新增了一个 FormData 对象,能够用于模拟表单。

FormData 简介

构造函数 FormData()

用于建立一个新的 FormData 对象。

语法

var formData = new FormData(form)
  • 参数

    • form 可选 - 一个 HTML 上的 <form> 表单元素。当使用 form 参数,建立的 FormData 对象会自动将 form 中的表单值也包含进去,文件内容会被编码

FormData 使用

首先,新建一个 FormData 对象:

var formData = new FormData();

而后,为它添加表单项:

formData.append('username', 'semlinker');
formData.append('id', 2005821040);

最后,直接传送这个FormData对象。这与提交网页表单的效果,彻底同样。

xhr.send(formData);

FormData 对象也能够用来获取网页表单的值。

var form = document.getElementById('myform'); // 获取页面上表单对象
var formData = new FormData(form);
formData.append('username', 'semlinker'); // 添加一个表单项
xhr.open('POST', form.action);
xhr.send(formData);

上传文件

新版 XMLHttpRequest 对象,不只能够发送文本信息,还能够上传文件。

1.为了上传文件, 咱们得先选中一个文件. 一个 type 为 file 的 input 输入框

<input id="input" type="file">

2.而后用 FormData 对象包裹选中的文件

var input = document.getElementById("input"),
    formData = new FormData();
formData.append("file",input.files[0]); // file名称与后台接收的名称一致

3.设置上传地址和请求方法

var url = "http://localhost:3000/upload",
    method = "POST";

4.发送 FormData 对象

xhr.send(formData);

跨域资源共享 (CORS)

新版本的 XMLHttpRequest 对象,能够向不一样域名的服务器发出 HTTP 请求。这叫作 "跨域资源共享"(Cross-origin resource sharing,简称 CORS)。

使用"跨域资源共享"的前提,是浏览器必须支持这个功能,并且服务器端必须赞成这种"跨域"。若是可以知足上面的条件,则代码的写法与不跨域的请求彻底同样。

xhr.open('GET', 'http://other.server/and/path/to/script');

接收二进制数据

XMLHttpRequest Level 1 XMLHttpRequest 对象只能处理文本数据,新版则能够处理二进制数据。从服务器取回二进制数据,较新的方法是使用新增的 responseType 属性。若是服务器返回文本数据,这个属性的值是 "TEXT",这是默认值。较新的浏览器还支持其余值,也就是说,能够接收其余格式的数据。

你能够把 responseType 设为 blob,表示服务器传回的是二进制对象。

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png');
xhr.responseType = 'blob';
xhr.send();

接收数据的时候,用浏览器自带的 Blob 对象便可。

一个  Blob 对象表示一个不可变的, 原始数据的相似文件对象。Blob 表示的数据不必定是一个 JavaScript 原生格式。 File 接口基于Blob,继承 blob功能并将其扩展为支持用户系统上的文件。

var blob = new Blob([xhr.response], {type: 'image/png'});

更多示例请参考 发送和接收二进制数据 。

进度信息

新版本的 XMLHttpRequest 对象,传送数据的时候,有一个 progress 事件,用来返回进度信息。

它分红上传和下载两种状况。下载的 progress 事件属于 XMLHttpRequest 对象,上传的 progress 事件属于XMLHttpRequest.upload 对象。

咱们先定义progress事件的回调函数:

xhr.onprogress = updateProgress;
xhr.upload.onprogress = updateProgress;

而后,在回调函数里面,使用这个事件的一些属性。

function updateProgress(event) {
  if (event.lengthComputable) {
    var percentComplete = event.loaded / event.total;
  }
}

上面的代码中,event.total 是须要传输的总字节,event.loaded 是已经传输的字节。若是event.lengthComputable 不为真,则 event.total 等于0。

各个浏览器 XMLHttpRequest Level 2 的兼容性 - Can I use/xhr2

XHR 下载数据

XHR 能够传输基于文本和二进制数据。实际上,浏览器能够为各类本地数据类型提供自动编码和解码,这样可让应用程序将这些类型直接传递给XHR,以便正确编码,反之亦然,这些类型能够由浏览器自动解码:

  • ArrayBuffer - 固定长度二进制数据缓冲区

  • Blob - 二进制不可变数据

  • Document - HTML或XML文档

  • JSON - JavaScript Object Notation

  • Text - 普通文本

XHR 下载图片示例:

var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://avatars2.githubusercontent.com/u/4220799?v=3');
    xhr.responseType = 'blob'; // 1

    xhr.onload = function() {
        if (this.status == 200) {
            var img = document.createElement('img');
            img.src = window.URL.createObjectURL(this.response); // 2
            img.onload = function() {
                window.URL.revokeObjectURL(this.src); //3
            };
            document.body.appendChild(img);
        }
    };
    xhr.send();

(1) 设置响应的数据类型为 blob

(2) 基于Blob建立一个惟一的对象URL,并做为图片的源地址 (URL.createObjectURL())

(3) 图片加载成功后释放对象的URL(URL.revokeObjectURL())

XHR 上传数据

经过 XHR 上传数据对于全部数据类型来讲都是简单而有效的。实际上,惟一的区别是当咱们在XHR请求中调用 send() 时,咱们需传递不一样的数据对象。其他的由浏览器处理:

var xhr = new XMLHttpRequest();
xhr.open('POST','/upload');
xhr.onload = function() { ... };
xhr.send("text string"); // 1

var formData = new FormData(); // 2
formData.append('id', 123456);
formData.append('topic', 'performance');

var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = function() { ... };
xhr.send(formData); // 3

var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = function() { ... };
var uInt8Array = new Uint8Array([1, 2, 3]); // 4
xhr.send(uInt8Array.buffer); // 5

(1) 发送普通的文本到服务器

(2) 经过 FormData API 建立动态表单

(3) 发送 FormData 数据到服务器

(4) 建立 Unit8Array 数组 (Uint8Array 数组类型表示一个8位无符号整型数组,建立时内容被初始化为0)

(5) 发送二进制数据到服务器

XHR send() 方法签名:

void send();
void send(ArrayBuffer data);
void send(Blob data);
void send(Document data);
void send(DOMString? data);
void send(FormData data);

除此以外,XHR 还支持大文件分块传输:

var blob = ...; // 1

const BYTES_PER_CHUNK = 1024 * 1024; // 2
const SIZE = blob.size;

var start = 0;
var end = BYTES_PER_CHUNK;

while(start < SIZE) { // 3
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/upload');
  xhr.onload = function() { ... };

  xhr.setRequestHeader('Content-Range', start+'-'+end+'/'+SIZE); // 4
  xhr.send(blob.slice(start, end)); // 5

  start = end;
  end = start + BYTES_PER_CHUNK;
}

(1) 一个任意的数据块 (二进制或文本)

(2) 将数据库大小设置为 1MB

(3) 迭代提供的数据,增量为1MB

(4) 设置上传的数据范围 (Content-Range请求头)

(5) 经过 XHR 上传 1MB 数据块

监听上传和下载进度

XHR 对象提供了一系列 API,用于监听进度事件,表示请求的当前状态:

事件类型 描述 触发次数
loadstart 开始传输 1次
progress 传输中 0次或屡次
error 传输中出现错误 0次或1次
abort 传输被用户取消 0次或1次
load 传输成功 0次或1次
loadend 传输完成 1次

每一个 XHR 传输都以 loadstart 事件开始,并以 loadend 事件结束,并在这两个事件期间触发一个或多个附加事件来指示传输的状态。所以,为了监控进度,应用程序能够在 XHR 对象上注册一组 JavaScript 事件侦听器:

var xhr = new XMLHttpRequest();
xhr.open('GET','/resource');
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
xhr.send();

(1) 设置请求超时时间为 5,000 ms (默认无超时时间)

(2) 注册成功回调

(3) 注册异常回调

(4) 计算已完成的进度

(5) 注册上传进度事件回调

(6) 注册下载进度事件回调

使用XHR流式传输数据

在某些状况下,应用程序可能须要或但愿逐步处理数据流:将数据上传到服务器,使其在客户机上可用,或者在从服务器下载数据时,进行流式处理。

var xhr = new XMLHttpRequest();
xhr.open('GET', '/stream');
xhr.seenBytes = 0;

xhr.onreadystatechange = function() {  // 1
  if(xhr.readyState > 2) {
    var newData = xhr.responseText.substr(xhr.seenBytes); // 2
    // process newData
    xhr.seenBytes = xhr.responseText.length; // 3
  }
};

xhr.send();

(1) 监听 onreadystatechange 事件

(2) 从部分响应中提取新数据

(3) 更新处理的字节偏移

这个例子能够在大多数现代浏览器中使用。可是,性能并很差,并且还有大量的注意事项和问题:

  • 请注意,咱们正在手动跟踪所看到字节的偏移量,而后手动分割数据:responseText 正在缓冲完整的响应!对于小的传输,这可能不是一个问题,但对于更大的下载,特别是在内存受限的设备,如手机,这是一个问题。释放缓冲响应的惟一方法是完成请求并打开一个新的请求。

  • 部分响应只能从 responseText 属性中读取,这将限制为仅限文本传输。没有办法读取二进制传输的部分响应。

  • 一旦读取了部分数据,咱们必须识别消息边界:应用程序逻辑必须定义本身的数据格式,而后缓冲并解析流以提取单个消息。

  • 浏览器在处理缓冲数据方面有所不一样:一些浏览器可能会当即释放数据,而其余浏览器可能会缓冲小的响应并等到积累到必定大小的数据块才释放它们。

  • 浏览器对不一样 Content-Type 资源类型的处理方式不一样,对于某些资源类型容许逐步读取 - 例如,text / html 类型,而其余 Content-Type 类型只能使用 application / x-javascript。

XHR 定时轮询

从服务器检索更新的最简单的策略之一是让客户端进行按期检查:客户端能够以周期性间隔(轮询服务器)启动后台XHR请求,以检查更新。若是新数据在服务器上可用,则在响应中返回,不然响应为空。

定时轮询的方式很简单,但若是定时间隔很短的话,也是很低效。所以设置合适的时间间隔显得相当重要:轮询间隔时间过长,会致使更新不及时,然而间隔时间太短的话,则会致使客户端与服务器没必要要的流程和高开销。接下来咱们来看一个简单的示例:

function checkUpdates(url) {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.onload = function() { ... }; // 1
  xhr.send();
}

setInterval(function() { checkUpdates('/updates') }, 60000); // 2

(1) 处理服务端接收的数据

(2) 设置定时轮询时间为 60s

定时轮询会产生如下的问题:

  • 每一个 XHR 请求都是一个独立的 HTTP 请求,平均来讲,HTTP 的请求头可能会引发大约 800 字节的开销 (不带HTTP cookie)。

每一个浏览器发起 HTTP 请求时都将携带额外的 500 - 800 字节的元数据 (请求头),如 user-agent、accept、Cache-Control 缓存控制头等。更糟糕的是,500 - 800 字节是理想的状况,若是携带 Cookies 信息,那么这个数值将会更大。总而言之,这些未压缩的 HTTP 元数据会引发很大开销。

  • 若是数据可以在间隔期间顺序到达,那么定时轮询能够正常工做。但咱们并无任何机制保证数据的正常接收。另外周期性轮询也将会引发服务器上可用的消息及其传送到客户端之间引入额外的延迟。简单的理解是若是有轮询期间有新的可用消息,客户端是不会立刻收到此新消息,而是要等到下一次轮询的时候,才能获取最新数据。

  • 除非仔细考虑,否则轮询一般会成为无线网络上昂贵的性能反模式。频繁地轮询会大量的消耗移动设备的电量。

轮询开销

平均每一个 HTTP 1.x 请求会增长大约 800字节的请求和响应开销 (详细信息能够查看 - Measuring and Controlling Protocol Overhead) 。另外在客户端登陆后,咱们还将产生一个额外的身份验证 cookie 和 消息ID; 假设这又增长了50个字节。所以,不返回新消息的请求将产生 850字节开销!如今假设咱们有10,000个客户端,全部的轮询间隔时间都是60秒:
$$
(850 bytes 8 bits 10,000) / 60 seconds ≈ 1.13 Mbps
$$
每一个客户端在每一个请求上发送 850 字节的数据,这转换为每秒 167 个请求,服务器上的吞吐量大约为 1.13 Mbps!这不是一个固定的值,此外该计算值仍是在假设服务器没有向任何客户端传递任何新的消息的理想状况下计算而得的。

XHR 长轮询

周期性轮询的挑战在于有可能进行许多没必要要的和空的检查。考虑到这一点,若是咱们对轮询工做流程进行了轻微的修改,而不是在没有更新可用的状况下返回一个空的响应,咱们能够保持链接空闲,直到更新可用吗?

(图片来源 - https://hpbn.co/xmlhttprequest/

经过保持长链接,直到更新可用,数据能够当即发送到客户端,一旦它在服务器上可用。所以,长时间轮询为消息延迟提供了最佳的状况,而且还消除了空检查,这减小了 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) 发起首次更新请求

那么长时间轮询老是比按期轮询更好的选择?除非消息到达率已知且不变,不然长轮询将始终提供更短的消息延迟。

另外一方面,开销讨论须要更细微的观点。首先,请注意,每一个传递的消息仍然引发相同的 HTTP 开销;每一个新消息都是独立的 HTTP 请求。可是,若是消息到达率高,那么长时间轮询会比按期轮询发出更多的XHR请求!

长轮询经过最小化消息延迟来动态地适应消息到达速率,这是您可能想要的或可能不须要的行为。若是对消息延迟要求不高的话,则定时轮询多是更有效的传输方式 - 例如,若是消息更新速率较高,则定时轮询提供简单的 "消息聚合" 机制 (即合并必定时间内的消息),这能够减小请求数量并提升移动设备的电池寿命。

XMLHttpRequest 库

Mock.js

Mock.js 是一款模拟数据生成器,旨在帮助前端攻城师独立于后端进行开发,帮助编写单元测试。提供了如下模拟功能:

  • 根据数据模板生成模拟数据

  • 模拟 Ajax 请求,生成并返回模拟数据

  • 基于 HTML 模板生成模拟数据

详细信息,请查看 - Mock.js 文档

Zone.js

Zone 是下一个 ECMAScript 规范的建议之一。Angular 团队实现了 JavaScript 版本的 zone.js ,它是用于拦截和跟踪异步工做的机制。

Zone 是一个全局的对象,用来配置有关如何拦截和跟踪异步回调的规则。Zone 有如下能力:

  • 拦截异步任务调度,如 setTimeout、setInterval、XMLHttpRequest 等

  • 提供了将数据附加到 zones 的方法

  • 为异常处理函数提供正确的上下文

  • 拦截阻塞的方法,如 alert、confirm 方法

zone.js 内部使用 Monkey Patch 方式,拦截 XMLHttpRequest.prototype 对象中的 open、send、abort 等方法。

// zone.js 源码片断
var openNative = patchMethod(window.XMLHttpRequest.prototype, 'open', function () { 
    return function (self, args) {
        self[XHR_SYNC] = args[2] == false;
        return openNative.apply(self, args);
    }; 
});

Oboe.js

Oboe.js 经过将 HTTP 请求-应答模型封装在一个渐进流式接口中,帮助网页应用快速应答。它将 streaming 和downloading 间的转换与SAX和DOM间JSON的解析整合在一块儿。它是个很是小的库,不依赖于其余程序库。它能够在 ajax 请求结束前就开始解析 json 变得十分容易,从而提升应用的应答速度。另外,它支持 Node.js 框架,还能够读入除了 http 外的其余流。

有兴趣的读者,推荐看一下官网的可交互的演示示例 - Why Oboe.js

(备注:该库就是文中 - 使用XHR流式传输数据章节的实际应用,不信往下看)

// oboe-browser.js 源码片断
function handleProgress() {            
    var textSoFar = xhr.responseText,
        newText = textSoFar.substr(numberOfCharsAlreadyGivenToCallback);
    if( newText ) {
        emitStreamData( newText );
    } 
    numberOfCharsAlreadyGivenToCallback = len(textSoFar);
}

fetch.js

fetch 函数是一个基于 Promise 的机制,用于在浏览器中以编程方式发送 Web 请求。该项目是实现标准 Fetch 规范的一个子集的 polyfill ,足以做为传统 Web 应用程序中 XMLHttpRequest 的代替品。

详细信息,请参考 - Github - fetch

Fetch API 兼容性,请参考 - Can I use Fetch

XMLHttpRequest 代码片断

ArrayBuffer 对象转为字符串

function ab2str(buf) {
  return String.fromCharCode.apply(null, new Uint16Array(buf));
}

代码片断来源 - ArrayBuffer与字符串的互相转换

字符串转 ArrayBuffer对象

function str2ab(str) {
  var buf = new ArrayBuffer(str.length * 2); // 每一个字符占用2个字节
  var bufView = new Uint16Array(buf);
  for (var i = 0, strLen = str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}

代码片断来源 - ArrayBuffer与字符串的互相转换

建立 XHR 对象

兼容全部浏览器

// Provide the XMLHttpRequest class for IE 5.x-6.x:
// Other browsers (including IE 7.x-8.x) ignore this
//   when XMLHttpRequest is predefined
var xmlHttp;
if (typeof XMLHttpRequest != "undefined") {
    xmlHttp = new XMLHttpRequest();
} else if (window.ActiveXObject) {
    var aVersions = ["Msxml2.XMLHttp.5.0", "Msxml2.XMLHttp.4.0", 
        "Msxml2.XMLHttp.3.0", "Msxml2.XMLHttp", "Microsoft.XMLHttp"];
    for (var i = 0; i < aVersions.length; i++) {
        try {
            xmlHttp = new ActiveXObject(aVersions[i]);
            break;
        } catch (e) {}
    }
}

精简版

var xmlHttp;
if (typeof XMLHttpRequest != "undefined") {
    xmlHttp = new XMLHttpRequest();
} else if (window.ActiveXObject) {
    try {
       xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
    } catch (e) {} 
}

sendAsBinary() polyfill

if (!XMLHttpRequest.prototype.sendAsBinary) {
  XMLHttpRequest.prototype.sendAsBinary = function (sData) {
    var nBytes = sData.length, ui8Data = new Uint8Array(nBytes);
    for (var nIdx = 0; nIdx < nBytes; nIdx++) {
      ui8Data[nIdx] = sData.charCodeAt(nIdx) & 0xff;
    }
    this.send(ui8Data);
  };
}

获取 XMLHttpRequest 响应体

function readBody(xhr) {
    var data;
    if (!xhr.responseType || xhr.responseType === "text") {
        data = xhr.responseText;
    } else if (xhr.responseType === "document") {
        data = xhr.responseXML;
    } else {
        data = xhr.response;
    }
    return data;
}

应用示例:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
        console.log(readBody(xhr));
    }
}
xhr.open('GET', 'https://www.baidu.com', true);
xhr.send(null);

获取 responseURL

export function getResponseURL(xhr: any): string {
  if ('responseURL' in xhr) {
    return xhr.responseURL;
  }
  if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) {
    return xhr.getResponseHeader('X-Request-URL');
  }
  return;
}

代码片断来源 - Github - @angular/http - http_utils.ts

验证请求是否成功

export const isSuccess = (status: number): boolean => (status >= 200 && status < 300);

代码片断来源 - Github - @angular/http - http_utils.ts

解析查询参数为Map对象

function paramParser(rawParams: string = ''): Map<string, string[]> {
  const map = new Map<string, string[]>();
  if (rawParams.length > 0) {
    const params: string[] = rawParams.split('&');
    params.forEach((param: string) => {
      const eqIdx = param.indexOf('=');
      const [key, val]: string[] =
          eqIdx == -1 ? [param, ''] : [param.slice(0, eqIdx), param.slice(eqIdx + 1)];
      const list = map.get(key) || [];
      list.push(val);
      map.set(key, list);
    });
  }
  return map;
}

代码片断来源 - Github - @angular/http - url_search_params.ts

ts 转换为 js 的代码以下:

function paramParser(rawParams) {
        if (rawParams === void 0) { rawParams = ''; }
        var map = new Map();
        if (rawParams.length > 0) {
            var params = rawParams.split('&');
            params.forEach(function (param) {
                var eqIdx = param.indexOf('=');
                var _a = eqIdx == -1 ? [param, ''] : 
                    [param.slice(0, eqIdx), param.slice(eqIdx + 1)], key = _a[0], 
                        val = _a[1];
                var list = map.get(key) || [];
                list.push(val);
                map.set(key, list);
            });
        }
        return map;
    }

XHR 下载图片

var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://avatars2.githubusercontent.com/u/4220799?v=3');
    xhr.responseType = 'blob';

    xhr.onload = function() {
        if (this.status == 200) {
            var img = document.createElement('img');
            img.src = window.URL.createObjectURL(this.response); 
            img.onload = function() {
                window.URL.revokeObjectURL(this.src); 
            };
            document.body.appendChild(img);
        }
    };
    xhr.send();

XHR 上传数据

发送普通文本

var xhr = new XMLHttpRequest();
xhr.open('POST','/upload');
xhr.onload = function() { ... };
xhr.send("text string");

发送FormData

var formData = new FormData(); 
formData.append('id', 123456);
formData.append('topic', 'performance');

var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = function() { ... };
xhr.send(formData);

发送 Buffer

var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = function() { ... };
var uInt8Array = new Uint8Array([1, 2, 3]); 
xhr.send(uInt8Array.buffer);

XHR 上传进度条

progress 元素

<progress id="uploadprogress" min="0" max="100" value="0">0</progress>

定义 progress 事件的回调函数

xhr.upload.onprogress = function (event) {
  if (event.lengthComputable) {
      var complete = (event.loaded / event.total * 100 | 0);
      var progress = document.getElementById('uploadprogress');
      progress.value = progress.innerHTML = complete;
  }
};

注意,progress事件不是定义在xhr,而是定义在xhr.upload,由于这里须要区分下载和上传,下载也有一个progress事件。

我有话说

1.什么状况下 XMLHttpRequest status 会为 0?

XMLHttpRequest 返回 status 时,会执行如下步骤:

  • 若是状态是 UNSENT 或 OPENED,则返回 0

  • 若是错误标志被设置,则返回 0

  • 不然返回 HTTP 状态码

另外当访问本地文件资源或在 Android 4.1 stock browser 中从应用缓存中获取文件时,XMLHttpRequest 的 status 值也会为0。

示例一:

var xmlhttp;
xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET","http://www.w3schools.com/XML/cd_catalog.xml", true);
xmlhttp.onreadystatechange=function() {
  if(xmlhttp.readyState == 4) console.log("status " + xmlhttp.status);
};
xmlhttp.addEventListener('error', function (error) {
   console.dir(error);
});
xmlhttp.send();

以上代码运行后,将会在控制台输出:

status 0
ProgressEvent # error 对象

2.为何 GET 或 HEAD 请求,不能经过 send() 方法发送请求体?

client . send([body = null])

Initiates the request. The optional argument provides the request body. The argument is ignored if request method is GET or HEAD. —— xhr.spec

经过 XMLHttpRequest 规范,咱们知道当请求方法是 GET 或 HEAD 时,send() 方法的 body 参数值将会被忽略。那么对于咱们经常使用的 GET 请求,咱们要怎么传递参数呢?解决参数传递可使用如下两种方式:

  • URL 传参 - 经常使用方式,有大小限制大约为 2KB

  • 请求头传参 - 通常用于传递 token 等认证信息

URL 传参

var url = "bla.php";
var params = "somevariable=somevalue&anothervariable=anothervalue";
var http = new XMLHttpRequest();

http.open("GET", url+"?"+params, true);
http.onreadystatechange = function()
{
    if(http.readyState == 4 && http.status == 200) {
        alert(http.responseText);
    }
}
http.send(null); // 请求方法是GET或HEAD时,设置请求体为空

在平常开发中,咱们最经常使用的方式是传递参数对象,所以咱们能够封装一个 formatParams() 来实现参数格式,具体示例以下:

formatParams() 函数:

function formatParams( params ){
  return "?" + Object
        .keys(params)
        .map(function(key){
          return key+"="+params[key]
        })
        .join("&")
}

应用示例:

var endpoint = "https://api.example.com/endpoint";
var params = {
  a: 1, 
  b: 2,
  c: 3
};
var url = endpoint + formatParams(params); // 实际应用中须要判断endpoint是否已经包含查询参数
// => "https://api.example.com/endpoint?a=1&b=2&c=3";

一些经常使用的 AJAX 库,如 jQuery、zepto 等,内部已经封装了参数序列化的方法 (如:jquery.param),咱们直接调用顶层的 API 方法便可。

(备注:以上示例来源 - stackoverflow - How do I pass along variables with XMLHttpRequest)

请求头传参 - (身份认证)

var xhr = new XMLHttpRequest();
xhr.open("POST", '/server', true);

xhr.setRequestHeader("x-access-token", "87a476494db6ec53d0a206589611aa3f");
xhr.onreadystatechange = function() {
    if(xhr.readyState == 4 && xhr.status == 200) {
       // handle data 
    }
};
xhr.send("foo=bar&lorem=ipsum");

详细的身份认证信息,请参考 - JSON Web Tokens

3.XMLHttpRequest 请求体支持哪些格式?

send() 方法签名:

void send();

void send(ArrayBuffer data);

void send(Blob data);

void send(Document data);

void send(DOMString? data);

void send(FormData data);

POST请求示例

发送 POST 请求一般须要如下步骤:

  • 使用 open() 方法打开链接时,设定 POST 请求方法和请求 URL地址

  • 设置正确的 Content-Type 请求头

  • 设置相关的事件监听

  • 设置请求体,并使用 send() 方法,发送请求

var xhr = new XMLHttpRequest();
xhr.open("POST", '/server', true);

//Send the proper header information along with the request
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

xhr.onreadystatechange = function() {
    if(xhr.readyState == 4 && xhr.status == 200) {
        // handle data
    }
}
xhr.send("foo=bar&lorem=ipsum"); 
// xhr.send('string'); 
// xhr.send(new Blob()); 
// xhr.send(new Int8Array()); 
// xhr.send({ form: 'data' }); 
// xhr.send(document);

4.什么是简单请求和预请求 (preflight request) ?

简单请求

一些不会触发 CORS preflight 的请求被称为 "简单请求",虽然 Fetch (定义 CORS的) 不使用这个术语。知足下述条件的就是 "简单请求":

预请求

不一样于上面讨论的简单请求,"预请求" 要求必须先发送一个 OPTIONS 方法请求给目的站点,来查明这个跨站请求对于目的站点是否是安全的可接受的。这样作,是由于跨站请求可能会对目的站点的数据产生影响。 当请求具有如下条件,就会被当成预请求处理:

详细的信息,请参考 - MDN - HTTP 访问控制 (CORS)

5.XMLHttpRequest 对象垃圾回收机制是什么?

在如下状况下,XMLHttpRequest 对象不会被垃圾回收:

  • 若是 XMLHttpRequest 对象的状态是 OPENED 且已设置 send() 的标识符

  • XMLHttpRequest 对象的状态是 HEADERS_RECEIVED (已获取响应头)

  • XMLHttpRequest 对象的状态是 LOADING (正在下载响应体),而且监听了如下一个或多个事件:readystatechange、progress、abort、error、load、timeout、loadend

若是 XMLHttpRequest 对象在链接尚存打开时被垃圾回收机制回收了,用户代理必须终止请求。

6.GET 和 POST 请求的区别?

  • 对于 GET 请求,浏览器会把 HTTP headers 和 data 一并发送出去,服务器响应 200。

  • 而对于 POST 请求,浏览器会先发送 HTTP headers,服务器响应 100 continue ,浏览器再发送 data,服务器响应 200。

详细的信息,请参考 - 99%的人都理解错了HTTP中GET与POST的区别

7.怎样防止重复发送 AJAX 请求?

  • setTimeout + clearTimeout - 连续的点击会把上一次点击清除掉,也就是ajax请求会在最后一次点击后发出去

  • disable 按钮

  • 缓存已成功的请求,若请求参数一致,则直接返回,不发送请求

详细的信息,请参考 - 知乎 - 怎样防止重复发送 Ajax 请求

八、AJAX 站点怎么作 SEO 优化

众所周知,大部分的搜索引擎爬虫都不会执行 JS,也就是说,若是页面内容由 Ajax 返回的话,搜索引擎是爬取不到部份内容的,也就无从作 SEO (搜索引擎优化)了。国外的 prerender.io 网站提供了一套比较成熟的方案,可是须要付费的。接下来咱们来看一下,怎么 PhantomJS 为咱们的站点作 SEO。

详细的信息,请参考 - 用PhantomJS来给AJAX站点作SEO优化

精品文章

参考资源