专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
OSC开源社区  ·  RAG市场的2024:随需而变,从狂热到理性 ·  2 天前  
程序猿  ·  41岁DeepMind天才科学家去世:长期受 ... ·  2 天前  
OSC开源社区  ·  宇树王兴兴早年创业分享引围观 ·  5 天前  
51好读  ›  专栏  ›  SegmentFault思否

你真的会使用 XMLHttpRequest 吗?

SegmentFault思否  · 公众号  · 程序员  · 2017-09-21 08:00

正文

看到标题时,有些同学可能会想:“我已经用 xhr 成功地发过很多个 Ajax 请求了,对它的基本操作已经算挺熟练了。” 我之前的想法和你们一样,直到最近我使用 xhr 时踩了不少坑儿,我才突然发现其实自己并不够了解 xhr ,我知道的只是最最基本的使用。

于是我决定好好地研究一番 xhr 的真面目,可拜读了不少博客后都不甚满意,于是我决定认真阅读一遍W3C的 XMLHttpRequest 标准。看完标准后我如同醍醐灌顶一般,感觉到了从未有过的清澈。这篇文章就是参考W3C的 XMLHttpRequest 标准和结合一些实践验证总结而来的。

Ajax 和 XMLHttpRequest

我们通常将 Ajax 等同于 XMLHttpRequest ,但细究起来它们两个是属于不同维度的2个概念。

以下是我认为对 Ajax 较为准确的解释:(摘自what is Ajax)

AJAX stands for Asynchronous JavaScript and XML. AJAX is a new technique for creating better, faster, and more interactive web applications with the help of XML, HTML, CSS, and Java Script.

AJAX is based on the following open standards:

  • Browser-based presentation using HTML and Cascading Style Sheets (CSS).

  • Data is stored in XML format and fetched from the server.

  • Behind-the-scenes data fetches using XMLHttpRequest objects in the browser.

  • JavaScript to make everything happen.

从上面的解释中可以知道: ajax 是一种技术方案,但并不是一种新技术。它依赖的是现有的 CSS / HTML / Javascript ,而其中最核心的依赖是浏览器提供的 XMLHttpRequest 对象,是这个对象使得浏览器可以发出 HTTP 请求与接收 HTTP 响应。

所以我用一句话来总结两者的关系:我们使用 XMLHttpRequest 对象来发送一个 Ajax 请求。

XMLHttpRequest 的发展历程

XMLHttpRequest 一开始只是微软浏览器提供的一个接口,后来各大浏览器纷纷效仿也提供了这个接口,再后来W3C对它进行了标准化,提出了 XMLHttpRequest 标准。 XMLHttpRequest 标准又分为 Level1 Level2

XMLHttpRequestLevel1 主要存在以下缺点:

  • 受同源策略的限制,不能发送跨域请求;

  • 不能发送二进制文件(如图片、视频、音频等),只能发送纯文本数据;

  • 在发送和获取数据的过程中,无法实时获取进度信息,只能判断是否完成;

那么 Level2 Level1 进行了改进, XMLHttpRequestLevel2 中新增了以下功能:

  • 可以发送跨域请求,在服务端允许的情况下;

  • 支持发送和接收二进制数据;

  • 新增formData对象,支持发送表单数据;

  • 发送和获取数据时,可以获取进度信息;

  • 可以设置请求的超时时间;

当然更详细的对比介绍,可以参考阮老师的这篇文章,文章中对新增的功能都有具体代码示例。

XMLHttpRequest 兼容性

关于 xhr 的浏览器兼容性,大家可以直接查看“Can I use”这个网站提供的结果XMLHttpRequest兼容性,下面提供一个截图。

从图中可以看到:

  • IE8/IE9、Opera Mini 完全不支持 xhr 对象

  • IE10/IE11部分支持,不支持 xhr.responseType json

  • 部分浏览器不支持设置请求超时,即无法使用 xhr.timeout

  • 部分浏览器不支持 xhr.responseType blob

细说 XMLHttpRequest 如何使用

先来看一段使用 XMLHttpRequest 发送 Ajax 请求的简单示例代码。

  1. function sendAjax() {

  2.  //构造表单数据

  3.  var formData = new FormData();

  4.  formData.append('username', 'johndoe');

  5.  formData.append('id', 123456);

  6.  //创建xhr对象

  7.  var xhr = new XMLHttpRequest();

  8.   //设置xhr请求的超时时间

  9.  xhr.timeout = 3000;

  10.  //设置响应返回的数据格式

  11.  xhr.responseType = "text";

  12.  //创建一个 post 请求,采用异步

  13.  xhr.open('POST', '/server', true);

  14.  //注册相关事件回调处理函数

  15.  xhr.onload = function(e) {

  16.    if(this.status == 200||this.status == 304){

  17.        alert(this.responseText);

  18.    }

  19.  };

  20.  xhr.ontimeout = function(e) { ... };

  21.  xhr.onerror = function(e) { ... };

  22.  xhr.upload.onprogress = function(e) { ... };

  23.  //发送数据

  24.  xhr.send(formData);

  25. }

上面是一个使用 xhr 发送表单数据的示例,整个流程可以参考注释。

接下来我将站在使用者的角度,以问题的形式介绍 xhr 的基本使用。我对每一个问题涉及到的知识点都会进行比较细致地介绍,有些知识点可能是你平时忽略关注的。

如何设置request header

在发送 Ajax 请求(实质是一个HTTP请求)时,我们可能需要设置一些请求头部信息,比如 content-type connection cookie accept-xxx 等。 xhr 提供了 setRequestHeader 来允许我们修改请求 header。

  1. void setRequestHeader(DOMString header, DOMString value);

注意点:

  • 方法的第一个参数 header 大小写不敏感,即可以写成 content-type ,也可以写成 Content-Type ,甚至写成 content-Type ;

  • Content-Type 的默认值与具体发送的数据类型有关,请参考本文【可以发送什么类型的数据】一节;

  • setRequestHeader 必须在 open() 方法之后, send() 方法之前调用,否则会抛错;

  • setRequestHeader 可以调用多次,最终的值不会采用覆盖 override 的方式,而是采用追加 append 的方式。下面是一个示例代码:

  1. var client = new XMLHttpRequest();

  2. client.open('GET', 'demo.cgi');

  3. client.setRequestHeader('X-Test', 'one');

  4. client.setRequestHeader('X-Test', 'two');

  5. // 最终request header中"X-Test"为: one, two

  6. client.send();

如何获取response header

xhr 提供了2个用来获取响应头部的方法: getAllResponseHeaders getResponseHeader 。前者是获取 response 中的所有header 字段,后者只是获取某个指定 header 字段的值。另外, getResponseHeader(header) header 参数不区分大小写。

  1. DOMString getAllResponseHeaders();

  2. DOMString getResponseHeader(DOMString header);

这2个方法看起来简单,但却处处是坑儿。

你是否遇到过下面的坑儿?——反正我是遇到了。。。

  1. 使用 getAllResponseHeaders() 看到的所有 response header 与实际在控制台 Network 中看到的 response header 不一样

  2. 使用 getResponseHeader() 获取某个 header 的值时,浏览器抛错 Refusedtogetunsafeheader"XXX"

经过一番寻找最终在 Stack Overflow找到了答案。

  • 原因1:W3C的 xhr 标准中做了限制,规定客户端无法获取 response 中的 Set-Cookie Set-Cookie2 这2个字段,无论是同域还是跨域请求;

  • 原因2:W3C 的 cors 标准对于跨域请求也做了限制,规定对于跨域请求,客户端允许获取的response header字段只限于“ simple response header ”和“ Access-Control-Expose-Headers ” (两个名词的解释见下方)。

simple response header 包括的 header 字段有: Cache-Control , Content-Language , Content-Type , Expires , Last-Modified , Pragma ;

Access-Control-Expose-Headers:首先得注意是" Access-Control-Expose-Headers "进行 跨域请求 时响应头部中的一个字段,对于同域请求,响应头部是没有这个字段的。这个字段中列举的 header 字段就是服务器允许暴露给客户端访问的字段。

所以 getAllResponseHeaders() 只能拿到 限制以外 (即被视为 safe )的header字段,而不是全部字段;而调用 getResponseHeader(header) 方法时, header 参数必须是 限制以外 的header字段,否则调用就会报 Refusedtogetunsafeheader 的错误。

如何指定 xhr.response 的数据类型

有些时候我们希望 xhr.response 返回的就是我们想要的数据类型。比如:响应返回的数据是纯JSON字符串,但我们期望最终通过 xhr.response 拿到的直接就是一个 js 对象,我们该怎么实现呢?

有2种方法可以实现,一个是 level1 就提供的 overrideMimeType() 方法,另一个是 level2 才提供的 xhr.responseType 属性。

xhr.overrideMimeType()

overrideMimeType xhr level1 就有的方法,所以浏览器兼容性良好。这个方法的作用就是用来重写 response content-type ,这样做有什么意义呢?比如:server 端给客户端返回了一份 document 或者是 xml 文档,我们希望最终通过 xhr.response 拿到的就是一个 DOM 对象,那么就可以用 xhr.overrideMimeType('text/xml; charset = utf-8') 来实现。

再举一个使用场景,我们都知道 xhr level1 不支持直接传输blob二进制数据,那如果真要传输 blob 该怎么办呢?当时就是利用 overrideMimeType 方法来解决这个问题的。

下面是一个获取图片文件的代码示例:

  1. var xhr = new XMLHttpRequest();

  2. //向 server 端获取一张图片

  3. xhr.open('GET', '/path/to/image.png', true);

  4. // 这行是关键!

  5. //将响应数据按照纯文本格式来解析,字符集替换为用户自己定义的字符集

  6. xhr.overrideMimeType( 'text/plain; charset=x-user-defined');

  7. xhr.onreadystatechange = function(e) {

  8.  if (this.readyState == 4 && this.status == 200) {

  9.    //通过 responseText 来获取图片文件对应的二进制字符串

  10.    var binStr = this.responseText;

  11.    //然后自己再想方法将逐个字节还原为二进制数据

  12.    for (var i = 0, len = binStr.length; i < len; ++i) {

  13.      var c = binStr.charCodeAt(i);

  14.       //String.fromCharCode(c & 0xff);

  15.      var byte = c & 0xff;

  16.    }

  17.  }

  18. };

  19. xhr.send();

代码示例中 xhr 请求的是一张图片,通过将 response content-type 改为'text/plain; charset=x-user-defined',使得 xhr 以纯文本格式来解析接收到的blob 数据,最终用户通过 this.responseText 拿到的就是图片文件对应的二进制字符串,最后再将其转换为 blob 数据。

xhr.responseType

responseType xhr level2 新增的属性,用来指定 xhr.response 的数据类型,目前还存在些兼容性问题,可以参考本文的【 XMLHttpRequest 的兼容性】这一小节。那么 responseType 可以设置为哪些格式呢,我简单做了一个表,如下:

xhr.response 数据类型 说明
"" String 字符串 默认值(在不设置 responseType 时)
"text" String 字符串
"document" Document 对象 希望返回 XML 格式数据时使用
"json" javascript 对象 存在兼容性问题,IE10/IE11不支持
"blob" Blob 对象
"arrayBuffer" ArrayBuffer 对象

下面是同样是获取一张图片的代码示例,相比 xhr.overrideMimeType ,用 xhr.response 来实现简单得多。

  1. var xhr = new XMLHttpRequest();

  2. xhr.open('GET', '/path/to/image.png', true);

  3. //可以将`xhr.responseType`设置为`"blob"`也可以设置为`" arrayBuffer"`

  4. //xhr.responseType = 'arrayBuffer';

  5. xhr.responseType = 'blob';

  6. xhr.onload = function(e) {

  7.  if (this.status == 200) {

  8.     var blob = this.response;

  9.    ...

  10.  }

  11. };

  12. xhr.send();

小结

虽然在 xhr level2 中,2者是共同存在的。但其实不难发现, xhr.responseType 就是用来取代 xhr.overrideMimeType() 的, xhr.responseType 功能强大的多, xhr.overrideMimeType() 能做到的 xhr.responseType 都能做到。所以我们现在完全可以摒弃使用 xhr.overrideMimeType() 了。

如何获取response数据

xhr 提供了3个属性来获取请求返回的数据,分别是: xhr.response xhr.responseText xhr.responseXML

  • xhr.response

    • 默认值:空字符串 ""

    • 当请求完成时,此属性才有正确的值

    • 请求未完成时,此属性的值可能是 "" 或者 null ,具体与 xhr.responseType 有关:当 responseType "" "text" 时,值为 "" responseType 为其他值时,值为 null

  • xhr.responseText

  • 默认值为空字符串 ""

  • 只有当 responseType "text" "" 时, xhr 对象上才有此属性,此时才能调用 xhr.responseText ,否则抛错

  • 只有当请求成功时,才能拿到正确值。以下2种情况下值都为空字符串 "" :请求未完成、请求失败

  • xhr.responseXML

    • 默认值为 null

    • 只有当 responseType "text" "" "document" 时, xhr 对象上才有此属性,此时才能调用 xhr.responseXML ,否则抛错

    • 只有当请求成功且返回数据被正确解析时,才能拿到正确值。以下3种情况下值都为 null :请求未完成、请求失败、请求成功但返回数据无法被正确解析时

如何追踪 ajax 请求的当前状态

在发一个 ajax 请求后,如果想追踪请求当前处于哪种状态,该怎么做呢?

xhr.readyState 这个属性即可追踪到。这个属性是只读属性,总共有5种可能值,分别对应 xhr 不同的不同阶段。每次 xhr.readyState 的值发生变化时,都会触发 xhr.onreadystatechange 事件,我们可以在这个事件中进行相关状态判断。

  1.  xhr.onreadystatechange = function () {

  2.    switch(xhr.readyState){

  3.      case 1://OPENED

  4.        //do something

  5.             break;

  6.      case 2://HEADERS_RECEIVED

  7.        //do something

  8.        break;

  9.      case 3://LOADING

  10.        //do something

  11.        break;

  12.       case 4://DONE

  13.        //do something

  14.        break;

  15.    }

说明:

状态 描述
0 UNSENT (初始状态,未打开) 此时 xhr 对象被成功构造, open() 方法还未被调用
1 OPENED (已打开,未发送) open() 方法已被成功调用, send() 方法还未被调用。注意:只有 xhr 处于 OPENED 状态,才能调用 xhr.setRequestHeader() xhr.send() ,否则会报错
2 HEADERS_RECEIVED (已获取响应头) send() 方法已经被调用, 响应头和响应状态已经返回
3 LOADING (正在下载响应体) 响应体( response entity body )正在下载中,此状态下通过 xhr.response 可能已经有了响应数据
4 DONE (整个数据传输过程结束) 整个数据传输过程结束,不管本次请求是成功还是失败
如何设置请求的超时时间

如果请求过了很久还没有成功,为了不会白白占用的网络资源,我们一般会主动终止请求。 XMLHttpRequest 提供了 timeout 属性来允许设置请求的超时时间。

  1. xhr.timeout

单位:milliseconds 毫秒

默认值: 0 ,即不设置超时

很多同学都知道:从请求开始算起,若超过 timeout 时间请求还没有结束(包括成功/失败),则会触发ontimeout事件,主动结束该请求。

那么到底什么时候才算是请求开始?

xhr.onloadstart 事件触发的时候,也就是你调用 xhr.send() 方法的时候。

因为 xhr.open() 只是创建了一个连接,但并没有真正开始数据的传输,而 xhr.send() 才是真正开始了数据的传输过程。只有调用了 xhr.send() ,才会触发 xhr.onloadstart

那么什么时候才算是请求结束?

xhr.loadend 事件触发的时候。

另外,还有2个需要注意的坑儿:

  1. 可以在 send() 之后再设置此 xhr.timeout ,但计时起始点仍为调用 xhr.send() 方法的时刻。

  2. xhr 为一个 sync 同步请求时, xhr.timeout 必须置为 0 ,否则会抛错。原因可以参考本文的【如何发一个同步请求】一节。

如何发一个同步请求

xhr 默认发的是异步请求,但也支持发同步请求(当然实际开发中应该尽量避免使用)。到底是异步还是同步请求,由 xhr.open() 传入的 async 参数决定。

  1. open(method, url [, async = true [, username = null [, password = null]]])

  • method : 请求的方式,如 GET/POST/HEADER 等,这个参数不区分大小写

  • url : 请求的地址,可以是相对地址如 example.php ,这个 相对 是相对于当前网页的 url 路径;也可以是绝对地址如 http://www.example.com/example.php

  • async : 默认值为 true ,即为异步请求,若 async=false ,则为同步请求

在我认真研读W3C 的 xhr 标准前,我总以为同步请求和异步请求只是阻塞和非阻塞的区别,其他什么事件触发、参数设置应该是一样的,事实证明我错了。

W3C 的 xhr标准中关于 open() 方法有这样一段说明:

Throws an "InvalidAccessError" exception if async is false, the JavaScript global environment is a document environment, and either the timeout attribute is not zero, the withCredentials attribute is true, or the responseType attribute is not the empty string.

从上面一段说明可以知道,当 xhr 为同步请求时,有如下限制:

  • xhr.timeout 必须为 0

  • xhr.withCredentials 必须为 false

  • xhr.responseType 必须为 "" (注意置为 "text" 也不允许)

若上面任何一个限制不满足,都会抛错,而对于异步请求,则没有这些参数设置上的限制。

之前说过页面中应该尽量避免使用 sync 同步请求,为什么呢? 因为我们无法设置请求超时时间( xhr.timeout 0 ,即不限时)。在不限制超时的情况下,有可能同步请求一直处于 pending 状态,服务端迟迟不返回响应,这样整个页面就会一直阻塞,无法响应用户的其他交互。

另外,标准中并没有提及同步请求时事件触发的限制,但实际开发中我确实遇到过部分应该触发的事件并没有触发的现象。如在 chrome中,当 xhr 为同步请求时,在 xhr.readyState 2 变成 3 时,并不会触发 onreadystatechange 事件, xhr.upload.onprogress xhr.onprogress 事件也不会触发。

如何获取上传、下载的进度

在上传或者下载比较大的文件时,实时显示当前的上传、下载进度是很普遍的产品需求。

我们可以通过 onprogress 事件来实时显示进度,默认情况下这个事件每50ms触发一次。需要注意的是,上传过程和下载过程触发的是不同对象的 onprogress 事件:

  • 上传触发的是 xhr.upload 对象的 onprogress 事件

  • 下载触发的是 xhr 对象的 onprogress 事件

  1. xhr.onprogress = updateProgress;

  2. xhr.upload.onprogress = updateProgress;

  3. function updateProgress(event) {

  4.     if (event.lengthComputable) {

  5.      var completedPercent = event.loaded / event.total;

  6.    }

  7. }

可以发送什么类型的数据
  1. void send(data);

xhr.send(data) 的参数data可以是以下几种类型:

  • ArrayBuffer

  • Blob

  • Document

  • DOMString

  • FormData

  • null

如果是 GET/HEAD请求, send() 方法一般不传参或传 null 。不过即使你真传入了参数,参数也最终被忽略, xhr.send(data) 中的data会被置为 null .

xhr.send(data) 中data参数的数据类型会影响请求头部 content-type 的默认值:

  • 如果 data Document 类型,同时也是 HTMLDocument 类型,则 content-type 默认值为 text/html;charset=UTF-8 ;否则为 application/xml;charset=UTF-8

  • 如果 data DOMString 类型, content-type 默认值为 text/plain;charset=UTF-8

  • 如果 data FormData 类型, content-type 默认值为 multipart/form-data;boundary=[xxx]

  • 如果 data 是其他类型,则不会设置 content-type 的默认值

当然这些只是 content-type 的默认值,但如果用 xhr.setRequestHeader() 手动设置了中 content-type 的值,以上默认值就会被覆盖。

另外需要注意的是,若在断网状态下调用 xhr.send(data) 方法,则会抛错: UncaughtNetworkError:Failedto execute'send'on'XMLHttpRequest' 。一旦程序抛出错误,如果不 catch 就无法继续执行后面的代码,所以调用 xhr.send(data) 方法时,应该用 try-catch 捕捉错误。

  1. try{

  2.    xhr.send(data)

  3.  } catch(e) {

  4.    //doSomething...

  5.  };

xhr.withCredentials 与 CORS 什么关系

我们都知道,在发同域请求时,浏览器会将 cookie 自动加在 request header 中。但大家是否遇到过这样的场景:在发送跨域请求时, cookie 并没有自动加在 request header 中。

造成这个问题的原因是:在 CORS 标准中做了规定,默认情况下,浏览器在发送跨域请求时,不能发送任何认证信息( credentials )如" cookies "和" HTTP authentication schemes "。除非 xhr.withCredentials true xhr 对象有一个属性叫 withCredentials ,默认值为 false )。

所以根本原因是 cookies 也是一种认证信息,在跨域请求中, client 端必须手动设置 xhr.withCredentials=true ,且 server 端也必须允许 request 能携带认证信息(即 response header 中包含 Access-Control-Allow-Credentials:true ),这样浏览器才会自动将 cookie 加在 request header 中。

另外,要特别注意一点,一旦跨域 request 能够携带认证信息, server 端一定不能将 Access-Control-Allow-Origin 设置为 * ,而必须设置为请求页面的域名。

xhr 相关事件

事件分类

xhr 相关事件有很多,有时记起来还挺容易混乱。但当我了解了具体代码实现后,就容易理清楚了。下面是 XMLHttpRequest 的部分实现代码:

  1. interface







请到「今天看啥」查看全文