Angular 4.x 修仙之路AJAX

你不知道的 XMLHttpRequest

2017-06-04  本文已影响459人  semlinker

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

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 实现

什么是 ActiveX 控件

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

ActiveXObject 对象

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

ActiveXObject 语法

newObj = new 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 对象

XMLHttpRequest

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

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

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();

属性

状态 描述
0 UNSENT (未打开) 表示已创建 XHR 对象,open() 方法还未被调用
1 OPENED (未发送) open() 方法已被成功调用,send() 方法还未被调用
2 HEADERS_RECEIVED (已获取响应头) send() 方法已经被调用,响应头和响应状态已经返回
3 LOADING (正在下载响应体) 响应体下载中,responseText中已经获取了部分数据
4 DONE (请求完成) 整个请求过程已经完毕
响应数据类型
"" (空字符串) 字符串(默认值)
"arraybuffer" ArrayBuffer
"blob" Blob
"document" Document
"json" JSON
"text" 字符串

方法

浏览器兼容性

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

事件

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 对象的主要属性:

XMLHttpRequest Level 1 缺点

XMLHttpRequest Level 2

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

设置超时时间

新版本 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)

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,以便正确编码,反之亦然,这些类型可以由浏览器自动解码:

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) 更新处理的字节偏移

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

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

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

每个浏览器发起 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 是一款模拟数据生成器,旨在帮助前端攻城师独立于后端进行开发,帮助编写单元测试。提供了以下模拟功能:

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

Zone.js

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

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

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 时,会执行以下步骤:

另外当访问本地文件资源或在 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 传参

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 请求通常需要以下步骤:

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 对象在连接尚存打开时被垃圾回收机制回收了,用户代理必须终止请求。

6.GET 和 POST 请求的区别?

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

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

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

8、AJAX 站点怎么做 SEO 优化

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

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

精品文章

参考资源

上一篇 下一篇

猜你喜欢

热点阅读