原文地址:

https://juejin.im/post/58e4a174ac502e006c1e18f4

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

  • AJAX、XMLHTTP、XMLHttpRequest详解、XMLHttpRequest Level 1、Level 2 详解
  • XHR 上传、下载数据、XHR 流式传输、XHR 定时轮询和长轮询区别与优缺点、XMLHttpRequest 库 (Mock.js、Zone.js、Oboe.js、fetch.js)
  • XMLHttpRequest 常用代码片段:
    • ArrayBuffer 对象转字符串
    • 字符串转 ArrayBuffer 对象
    • 创建 XHR 对象
    • sendAsBinary() polyfill
    • 获取 XMLHttpRequest 响应体
    • 获取 responseURL
    • 验证请求是否成功
    • 解析查询参数为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 BrowsersChrome & Firefox1NodeConcise SyntaxPromisesNative2Single Purpose3Formal Specification
XMLHttpRequest   
Node HTTP    
fetch()  
Fetch polyfill  
node-fetch   
isomorphic-fetch 
superagent   
axios  
request     
jQuery     
reqwest 

Chrome & Firefox are listed separately because they support fetch()caniuse.com/fetch
Native: Meaning you can just use it - no need to include a library.
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 - 用于表示请求的五种状态:
状态描述
0UNSENT (未打开)表示已创建 XHR 对象,open() 方法还未被调用
1OPENED (未发送)open() 方法已被成功调用,send() 方法还未被调用
2HEADERS_RECEIVED (已获取响应头)send() 方法已经被调用,响应头和响应状态已经返回
3LOADING (正在下载响应体)响应体下载中,responseText中已经获取了部分数据
4DONE (请求完成)整个请求过程已经完毕
  • 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。注意:使用该方法获取某些响应头时,浏览器会抛出异常,具体原因如下:

  • 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 异常
      • 如果请求方法(不区分大小写)为 CONNECTTRACE 或 TRACK 将会抛出 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
FeatureChromeFirefox (Gecko)Internet ExplorerOperaSafari (WebKit)
Basic support (XHR1)11.05 (via ActiveXObject)7 (XMLHttpRequest)(Yes)1.2
send(ArrayBuffer)99?11.60?
send(Blob)73.6?12?
send(FormData)64?12?
response1061011.60?
responseType = 'arraybuffer'1061011.60?
responseType = 'blob'1961012?
responseType = 'document'1811未实现未实现未实现
responseType = 'json'未实现10未实现12未实现
Progress Events73.51012?
withCredentials33.510124

事件

  • 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 请求

8、AJAX 站点怎么做 SEO 优化

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

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

精品文章

参考资源