【javascript】XMLHttpRequest 详解
<h2><code>一、XMLHttpRequest</code>的发展历程</h2><p><code>XMLHttpRequest</code>一开始只是微软浏览器提供的一个接口,后来各大浏览器纷纷效仿也提供了这个接口,再后来W3C对它进行了标准化,提出了<code>XMLHttpRequest</code>标准。<code>XMLHttpRequest</code>标准又分为<code>Level 1</code>和<code>Level 2</code>。<br><code>XMLHttpRequest Level 1</code>主要存在以下缺点:</p>
<ul>
<li>
<p>受同源策略的限制,不能发送跨域请求;</p>
</li>
<li>
<p>不能发送二进制文件(如图片、视频、音频等),只能发送纯文本数据;</p>
</li>
<li>
<p>在发送和获取数据的过程中,无法实时获取进度信息,只能判断是否完成;</p>
</li>
</ul>
<p>那么<code>Level 2</code>对<code>Level 1</code> 进行了改进,<code>XMLHttpRequest Level 2</code>中新增了以下功能:</p>
<ul>
<li>
<p>可以发送跨域请求,在服务端允许的情况下;</p>
</li>
<li>
<p>支持发送和接收二进制数据;</p>
</li>
<li>
<p>新增formData对象,支持发送表单数据;</p>
</li>
<li>
<p>发送和获取数据时,可以获取进度信息;</p>
</li>
<li>
<p>可以设置请求的超时时间;</p>
</li>
</ul>
<p>当然更详细的对比介绍,可以参考阮老师的这篇文章,文章中对新增的功能都有具体代码示例。</p>
<h2><code>二、XMLHttpRequest</code>兼容性</h2>
<p>关于<code>xhr</code>的浏览器兼容性,大家可以直接查看“Can I use”这个网站提供的结果XMLHttpRequest兼容性:</p>
<ul>
<li>
<p>IE8/IE9、Opera Mini 完全不支持<code>xhr</code>对象</p>
</li>
<li>
<p>IE10/IE11部分支持,不支持 <code>xhr.responseType</code>为<code>json</code></p>
</li>
<li>
<p>部分浏览器不支持设置请求超时,即无法使用<code>xhr.timeout</code></p>
</li>
<li>
<p>部分浏览器不支持<code>xhr.responseType</code>为<code>blob</code></p>
</li>
</ul>
<h2>三、细说<code>XMLHttpRequest</code>如何使用</h2>
<p>先来看一段使用<code>XMLHttpRequest</code>发送<code>Ajax</code>请求的简单示例代码。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> sendAjax() {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">构造表单数据</span>
<span style="color: rgba(0, 0, 255, 1)">var</span> formData = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> FormData();
formData.append(</span>'username', 'johndoe'<span style="color: rgba(0, 0, 0, 1)">);
formData.append(</span>'id', 123456<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">创建xhr对象 </span>
<span style="color: rgba(0, 0, 255, 1)">var</span> xhr = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> XMLHttpRequest();
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">设置xhr请求的超时时间</span>
xhr.timeout = 3000<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">设置响应返回的数据格式</span>
xhr.responseType = "text"<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">创建一个 post 请求,采用异步</span>
xhr.open('POST', '/server', <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">注册相关事件回调处理函数</span>
xhr.onload = <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(e) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span>(<span style="color: rgba(0, 0, 255, 1)">this</span>.status == 200||<span style="color: rgba(0, 0, 255, 1)">this</span>.status == 304<span style="color: rgba(0, 0, 0, 1)">){
alert(</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.responseText);
}
};
xhr.ontimeout </span>= <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(e) { ... };
xhr.onerror </span>= <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(e) { ... };
xhr.upload.onprogress </span>= <span style="color: rgba(0, 0, 255, 1)">function</span>(e) { ... };</pre>
</div>
<p>上面是一个使用<code>xhr</code>发送表单数据的示例,整个流程可以参考注释。</p>
<h3>1、如何设置request header</h3>
<p>在发送<code>Ajax</code>请求(实质是一个HTTP请求)时,我们可能需要设置一些请求头部信息,比如<code>content-type</code>、<code>connection</code>、<code>cookie</code>、<code>accept-xxx</code>等。<code>xhr</code>提供了<code>setRequestHeader</code>来允许我们修改请求 header。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">void</span> setRequestHeader(DOMString header, DOMString value);</pre>
</div>
<p>注意点:</p>
<ul>
<li>
<p>方法的第一个参数 header 大小写不敏感,即可以写成<code>content-type</code>,也可以写成<code>Content-Type</code>,甚至写成<code>content-Type</code>;</p>
</li>
<li>
<p><code>Content-Type</code>的默认值与具体发送的数据类型有关,请参考本文【可以发送什么类型的数据】一节;</p>
</li>
<li>
<p><code>setRequestHeader</code>必须在<code>open()</code>方法之后,<code>send()</code>方法之前调用,否则会抛错;</p>
</li>
<li>
<p><code>setRequestHeader</code>可以调用多次,最终的值不会采用覆盖<code>override</code>的方式,而是采用追加<code>append</code>的方式。下面是一个示例代码:</p>
</li>
</ul>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">var</span> client = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> XMLHttpRequest();
client.open(</span>'GET', 'demo.cgi'<span style="color: rgba(0, 0, 0, 1)">);
client.setRequestHeader(</span>'X-Test', 'one'<span style="color: rgba(0, 0, 0, 1)">);
client.setRequestHeader(</span>'X-Test', 'two'<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 最终request header中"X-Test"为: one, two</span>
client.send();</pre>
</div>
<h3>2、如何获取response header</h3>
<p><code>xhr</code>提供了2个用来获取响应头部的方法:<code>getAllResponseHeaders</code>和<code>getResponseHeader</code>。前者是获取 response 中的所有header 字段,后者只是获取某个指定 header 字段的值。另外,<code>getResponseHeader(header)</code>的<code>header</code>参数不区分大小写。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">DOMString getAllResponseHeaders();
DOMString getResponseHeader(DOMString header);</span></pre>
</div>
<p>这2个方法看起来简单,但却处处是坑儿。</p>
<p>你是否遇到过下面的坑儿?——反正我是遇到了。。。</p>
<ol>
<li>
<p>使用<code>getAllResponseHeaders()</code>看到的所有<code>response header</code>与实际在控制台 <code>Network</code> 中看到的 <code>response header</code>不一样</p>
</li>
<li>
<p>使用<code>getResponseHeader()</code>获取某个 <code>header</code> 的值时,浏览器抛错<code>Refused to get unsafe header "XXX"</code></p>
</li>
</ol>
<p>经过一番寻找最终在 Stack Overflow找到了答案。</p>
<ul>
<li>
<p>原因1:W3C的 xhr 标准中做了限制,规定客户端无法获取 response 中的 <code>Set-Cookie</code>、<code>Set-Cookie2</code>这2个字段,无论是同域还是跨域请求;</p>
</li>
<li>
<p>原因2:W3C 的 cors 标准对于跨域请求也做了限制,规定对于跨域请求,客户端允许获取的response header字段只限于“<code>simple response header</code>”和“<code>Access-Control-Expose-Headers</code>” (两个名词的解释见下方)。</p>
</li>
</ul>
<blockquote>
<p>"<code>simple response header</code>"包括的 header 字段有:<code>Cache-Control</code>,<code>Content-Language</code>,<code>Content-Type</code>,<code>Expires</code>,<code>Last-Modified</code>,<code>Pragma</code>;<br>"<code>Access-Control-Expose-Headers</code>":首先得注意是"<code>Access-Control-Expose-Headers</code>"进行跨域请求时响应头部中的一个字段,对于同域请求,响应头部是没有这个字段的。这个字段中列举的 header 字段就是服务器允许暴露给客户端访问的字段。</p>
</blockquote>
<p>所以<code>getAllResponseHeaders()</code>只能拿到<em>限制以外</em>(即被视为<code>safe</code>)的header字段,而不是全部字段;而调用<code>getResponseHeader(header)</code>方法时,<code>header</code>参数必须是<em>限制以外</em>的header字段,否则调用就会报<code>Refused to get unsafe header</code>的错误。</p>
<h3>3、如何指定<code>xhr.response</code>的数据类型</h3>
<p>有些时候我们希望<code>xhr.response</code>返回的就是我们想要的数据类型。比如:响应返回的数据是纯JSON字符串,但我们期望最终通过<code>xhr.response</code>拿到的直接就是一个 js 对象,我们该怎么实现呢?<br>有2种方法可以实现,一个是<code>level 1</code>就提供的<code>overrideMimeType()</code>方法,另一个是<code>level 2</code>才提供的<code>xhr.responseType</code>属性。</p>
<h4><code>xhr.overrideMimeType()</code></h4>
<p><code>overrideMimeType</code>是<code>xhr level 1</code>就有的方法,所以浏览器兼容性良好。这个方法的作用就是用来重写<code>response</code>的<code>content-type</code>,这样做有什么意义呢?比如:server 端给客户端返回了一份<code>document</code>或者是 <code>xml</code>文档,我们希望最终通过<code>xhr.response</code>拿到的就是一个<code>DOM</code>对象,那么就可以用<code>xhr.overrideMimeType('text/xml; charset = utf-8')</code>来实现。</p>
<p>再举一个使用场景,我们都知道<code>xhr level 1</code>不支持直接传输blob二进制数据,那如果真要传输 blob 该怎么办呢?当时就是利用<code>overrideMimeType</code>方法来解决这个问题的。</p>
<p>下面是一个获取图片文件的代码示例:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">var</span> xhr = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> XMLHttpRequest();
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">向 server 端获取一张图片</span>
xhr.open('GET', '/path/to/image.png', <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 这行是关键!</span><span style="color: rgba(0, 128, 0, 1)">
//</span><span style="color: rgba(0, 128, 0, 1)">将响应数据按照纯文本格式来解析,字符集替换为用户自己定义的字符集</span>
xhr.overrideMimeType('text/plain; charset=x-user-defined'<span style="color: rgba(0, 0, 0, 1)">);
xhr.onreadystatechange </span>= <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(e) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span>.readyState == 4 && <span style="color: rgba(0, 0, 255, 1)">this</span>.status == 200<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">通过 responseText 来获取图片文件对应的二进制字符串</span>
<span style="color: rgba(0, 0, 255, 1)">var</span> binStr = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.responseText;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">然后自己再想方法将逐个字节还原为二进制数据</span>
<span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">var</span> i = 0, len = binStr.length; i < len; ++<span style="color: rgba(0, 0, 0, 1)">i) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> c =<span style="color: rgba(0, 0, 0, 1)"> binStr.charCodeAt(i);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">String.fromCharCode(c & 0xff);</span>
<span style="color: rgba(0, 0, 255, 1)">var</span> <span style="color: rgba(0, 0, 255, 1)">byte</span> = c & 0xff<span style="color: rgba(0, 0, 0, 1)">;
}
}
};
xhr.send();</span></pre>
</div>
<p>代码示例中<code>xhr</code>请求的是一张图片,通过将 <code>response</code> 的 <code>content-type</code> 改为'text/plain; charset=x-user-defined',使得 <code>xhr</code> 以纯文本格式来解析接收到的blob 数据,最终用户通过<code>this.responseText</code>拿到的就是图片文件对应的二进制字符串,最后再将其转换为 blob 数据。</p>
<h4><code>xhr.responseType</code></h4>
<p><code>responseType</code>是<code>xhr level 2</code>新增的属性,用来指定<code>xhr.response</code>的数据类型,目前还存在些兼容性问题,可以参考本文的【<code>XMLHttpRequest</code>的兼容性】这一小节。那么<code>responseType</code>可以设置为哪些格式呢,我简单做了一个表,如下:</p>
<table>
<thead>
<tr><th>值</th><th><code>xhr.response</code> 数据类型</th><th>说明</th></tr>
</thead>
<tbody>
<tr>
<td><code>""</code></td>
<td><code>String</code>字符串</td>
<td>默认值(在不设置<code>responseType</code>时)</td>
</tr>
<tr>
<td><code>"text"</code></td>
<td><code>String</code>字符串</td>
</tr>
<tr>
<td><code>"document"</code></td>
<td><code>Document</code>对象</td>
<td>希望返回 <code>XML</code> 格式数据时使用</td>
</tr>
<tr>
<td><code>"json"</code></td>
<td><code>javascript</code> 对象</td>
<td>存在兼容性问题,IE10/IE11不支持</td>
</tr>
<tr>
<td><code>"blob"</code></td>
<td><code>Blob</code>对象</td>
</tr>
<tr>
<td><code>"arrayBuffer"</code></td>
<td><code>ArrayBuffer</code>对象</td>
</tr>
</tbody>
</table>
<p>下面是同样是获取一张图片的代码示例,相比<code>xhr.overrideMimeType</code>,用<code>xhr.response</code>来实现简单得多。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">var</span> xhr = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> XMLHttpRequest();
xhr.open(</span>'GET', '/path/to/image.png', <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">可以将`xhr.responseType`设置为`"blob"`也可以设置为`" arrayBuffer"`</span><span style="color: rgba(0, 128, 0, 1)">
//</span><span style="color: rgba(0, 128, 0, 1)">xhr.responseType = 'arrayBuffer';</span>
xhr.responseType = 'blob'<span style="color: rgba(0, 0, 0, 1)">;
xhr.onload </span>= <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(e) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span>.status == 200<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> blob = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.response;
...
}
};
xhr.send();</span></pre>
</div>
<h4>小结</h4>
<p>虽然在<code>xhr level 2</code>中,2者是共同存在的。但其实不难发现,<code>xhr.responseType</code>就是用来取代<code>xhr.overrideMimeType()</code>的,<code>xhr.responseType</code>功能强大的多,<code>xhr.overrideMimeType()</code>能做到的<code>xhr.responseType</code>都能做到。所以我们现在完全可以摒弃使用<code>xhr.overrideMimeType()</code>了。</p>
<h3>4、如何获取response数据</h3>
<p><code>xhr</code>提供了3个属性来获取请求返回的数据,分别是:<code>xhr.response</code>、<code>xhr.responseText</code>、<code>xhr.responseXML</code></p>
<ul>
<li>
<p><code>xhr.response</code></p>
<ul>
<li>
<p>默认值:空字符串<code>""</code></p>
</li>
<li>
<p>当请求完成时,此属性才有正确的值</p>
</li>
<li>
<p>请求未完成时,此属性的值可能是<code>""</code>或者 <code>null</code>,具体与 <code>xhr.responseType</code>有关:当<code>responseType</code>为<code>""</code>或<code>"text"</code>时,值为<code>""</code>;<code>responseType</code>为其他值时,值为 <code>null</code></p>
</li>
</ul>
</li>
<li>
<p><code>xhr.responseText</code></p>
<ul>
<li>
<p>默认值为空字符串<code>""</code></p>
</li>
<li>
<p>只有当 <code>responseType</code> 为<code>"text"</code>、<code>""</code>时,<code>xhr</code>对象上才有此属性,此时才能调用<code>xhr.responseText</code>,否则抛错</p>
</li>
<li>
<p>只有当请求成功时,才能拿到正确值。以下2种情况下值都为空字符串<code>""</code>:请求未完成、请求失败</p>
</li>
</ul>
</li>
<li>
<p><code>xhr.responseXML</code></p>
<ul>
<li>
<p>默认值为 <code>null</code></p>
</li>
<li>
<p>只有当 <code>responseType</code> 为<code>"text"</code>、<code>""</code>、<code>"document"</code>时,<code>xhr</code>对象上才有此属性,此时才能调用<code>xhr.responseXML</code>,否则抛错</p>
</li>
<li>
<p>只有当请求成功且返回数据被正确解析时,才能拿到正确值。以下3种情况下值都为<code>null</code>:请求未完成、请求失败、请求成功但返回数据无法被正确解析时</p>
</li>
</ul>
</li>
</ul>
<h3>5、如何追踪<code>ajax</code>请求的当前状态</h3>
<p>在发一个<code>ajax</code>请求后,如果想追踪请求当前处于哪种状态,该怎么做呢?</p>
<p>用<code>xhr.readyState</code>这个属性即可追踪到。这个属性是只读属性,总共有5种可能值,分别对应<code>xhr</code>不同的不同阶段。每次<code>xhr.readyState</code>的值发生变化时,都会触发<code>xhr.onreadystatechange</code>事件,我们可以在这个事件中进行相关状态判断。</p>
<div class="cnblogs_code">
<pre>xhr.onreadystatechange = <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> () {
</span><span style="color: rgba(0, 0, 255, 1)">switch</span><span style="color: rgba(0, 0, 0, 1)">(xhr.readyState){
</span><span style="color: rgba(0, 0, 255, 1)">case</span> 1:<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">OPENED</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">do something</span>
<span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">case</span> 2:<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">HEADERS_RECEIVED</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">do something</span>
<span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">case</span> 3:<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">LOADING</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">do something</span>
<span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">case</span> 4:<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">DONE</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">do something</span>
<span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">;
}</span></pre>
</div>
<table>
<thead>
<tr><th>值</th><th>状态</th><th>描述</th></tr>
</thead>
<tbody>
<tr>
<td><code>0</code></td>
<td><code>UNSENT</code> (初始状态,未打开)</td>
<td>此时<code>xhr</code>对象被成功构造,<code>open()</code>方法还未被调用</td>
</tr>
<tr>
<td><code>1</code></td>
<td><code>OPENED</code> (已打开,未发送)</td>
<td><code>open()</code>方法已被成功调用,<code>send()</code>方法还未被调用。注意:只有<code>xhr</code>处于<code>OPENED</code>状态,才能调用<code>xhr.setRequestHeader()</code>和<code>xhr.send()</code>,否则会报错</td>
</tr>
<tr>
<td><code>2</code></td>
<td><code>HEADERS_RECEIVED</code>(已获取响应头)</td>
<td><code>send()</code>方法已经被调用, 响应头和响应状态已经返回</td>
</tr>
<tr>
<td><code>3</code></td>
<td><code>LOADING</code> (正在下载响应体)</td>
<td>响应体(<code>response entity body</code>)正在下载中,此状态下通过<code>xhr.response</code>可能已经有了响应数据</td>
</tr>
<tr>
<td><code>4</code></td>
<td><code>DONE</code> (整个数据传输过程结束)</td>
<td>整个数据传输过程结束,不管本次请求是成功还是失败</td>
</tr>
</tbody>
</table>
<h3>6、如何设置请求的超时时间</h3>
<p>如果请求过了很久还没有成功,为了不会白白占用的网络资源,我们一般会主动终止请求。<code>XMLHttpRequest</code>提供了<code>timeout</code>属性来允许设置请求的超时时间。</p>
<div class="cnblogs_code">
<pre>xhr.timeout</pre>
</div>
<p>单位:milliseconds 毫秒<br>默认值:<code>0</code>,即不设置超时</p>
<p>很多同学都知道:从<em>请求开始</em> 算起,若超过 <code>timeout</code> 时间请求还没有结束(包括成功/失败),则会触发ontimeout事件,主动结束该请求。</p>
<p>【那么到底什么时候才算是<em>请求开始</em> ?】<br>——<code>xhr.onloadstart</code>事件触发的时候,也就是你调用<code>xhr.send()</code>方法的时候。<br>因为<code>xhr.open()</code>只是创建了一个连接,但并没有真正开始数据的传输,而<code>xhr.send()</code>才是真正开始了数据的传输过程。只有调用了<code>xhr.send()</code>,才会触发<code>xhr.onloadstart</code> 。</p>
<p>【那么什么时候才算是<em>请求结束</em> ?】<br>—— <code>xhr.loadend</code>事件触发的时候。</p>
<p>另外,还有2个需要注意的坑儿:</p>
<ol>
<li>
<p>可以在 <code>send()</code>之后再设置此<code>xhr.timeout</code>,但计时起始点仍为调用<code>xhr.send()</code>方法的时刻。</p>
</li>
<li>
<p>当<code>xhr</code>为一个<code>sync</code>同步请求时,<code>xhr.timeout</code>必须置为<code>0</code>,否则会抛错。原因可以参考本文的【如何发一个同步请求】一节。</p>
</li>
</ol>
<h3>7、如何发一个同步请求</h3>
<p><code>xhr</code>默认发的是异步请求,但也支持发同步请求(当然实际开发中应该尽量避免使用)。到底是异步还是同步请求,由<code>xhr.open()</code>传入的<code>async</code>参数决定。</p>
<div class="cnblogs_code">
<pre>open(method, url [, async = <span style="color: rgba(0, 0, 255, 1)">true</span> [, username = <span style="color: rgba(0, 0, 255, 1)">null</span> [, password = <span style="color: rgba(0, 0, 255, 1)">null</span>]]])</pre>
</div>
<ul>
<li>
<p><code>method</code>: 请求的方式,如<code>GET/POST/HEADER</code>等,这个参数不区分大小写</p>
</li>
<li>
<p><code>url</code>: 请求的地址,可以是相对地址如<code>example.php</code>,这个相对是相对于当前网页的<code>url</code>路径;也可以是绝对地址如<code>http://www.example.com/example.php</code></p>
</li>
<li>
<p><code>async</code>: 默认值为<code>true</code>,即为异步请求,若<code>async=false</code>,则为同步请求</p>
</li>
</ul>
<p>在我认真研读W3C 的 xhr 标准前,我总以为同步请求和异步请求只是阻塞和非阻塞的区别,其他什么事件触发、参数设置应该是一样的,事实证明我错了。</p>
<p>W3C 的 xhr标准中关于<code>open()</code>方法有这样一段说明:</p>
<div class="cnblogs_code">
<pre>Throws an "InvalidAccessError" exception <span style="color: rgba(0, 0, 255, 1)">if</span> async is <span style="color: rgba(0, 0, 255, 1)">false</span>, the JavaScript global environment is a document environment, and either the timeout attribute is not zero, the withCredentials attribute is <span style="color: rgba(0, 0, 255, 1)">true</span>, or the responseType attribute is not the empty string.</pre>
</div>
<p>从上面一段说明可以知道,当<code>xhr</code>为同步请求时,有如下限制:</p>
<ul>
<li>
<p><code>xhr.timeout</code>必须为<code>0</code></p>
</li>
<li>
<p><code>xhr.withCredentials</code>必须为 <code>false</code></p>
</li>
<li>
<p><code>xhr.responseType</code>必须为<code>""</code>(注意置为<code>"text"</code>也不允许)</p>
</li>
</ul>
<p>若上面任何一个限制不满足,都会抛错,而对于异步请求,则没有这些参数设置上的限制。</p>
<p>之前说过页面中应该尽量避免使用<code>sync</code>同步请求,为什么呢?<br>因为我们无法设置请求超时时间(<code>xhr.timeout</code>为<code>0</code>,即不限时)。在不限制超时的情况下,有可能同步请求一直处于<code>pending</code>状态,服务端迟迟不返回响应,这样整个页面就会一直阻塞,无法响应用户的其他交互。</p>
<p>另外,标准中并没有提及同步请求时事件触发的限制,但实际开发中我确实遇到过部分应该触发的事件并没有触发的现象。如在 chrome中,当<code>xhr</code>为同步请求时,在<code>xhr.readyState</code>由<code>2</code>变成<code>3</code>时,并不会触发 <code>onreadystatechange</code>事件,<code>xhr.upload.onprogress</code>和 <code>xhr.onprogress</code>事件也不会触发。</p>
<h3>8、如何获取上传、下载的进度</h3>
<p>在上传或者下载比较大的文件时,实时显示当前的上传、下载进度是很普遍的产品需求。<br>我们可以通过<code>onprogress</code>事件来实时显示进度,默认情况下这个事件每50ms触发一次。需要注意的是,上传过程和下载过程触发的是不同对象的<code>onprogress</code>事件:</p>
<ul>
<ul>
<li>
<p>上传触发的是<code>xhr.upload</code>对象的 <code>onprogress</code>事件</p>
</li>
<li>
<p>下载触发的是<code>xhr</code>对象的<code>onprogress</code>事件</p>
</li>
</ul>
</ul>
<div class="cnblogs_code">
<pre>xhr.onprogress =<span style="color: rgba(0, 0, 0, 1)"> updateProgress;
xhr.upload.onprogress </span>=<span style="color: rgba(0, 0, 0, 1)"> updateProgress;
</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> updateProgress(event) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (event.lengthComputable) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> completedPercent = event.loaded /<span style="color: rgba(0, 0, 0, 1)"> event.total;
}
}</span></pre>
</div>
<h3>9、可以发送什么类型的数据</h3>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">void</span> send(data);</pre>
</div>
<p><code>xhr.send(data)</code>的参数data可以是以下几种类型:</p>
<ul>
<li>
<p><code>ArrayBuffer</code></p>
</li>
<li>
<p><code>Blob</code></p>
</li>
<li>
<p><code>Document</code></p>
</li>
<li>
<p><code>DOMString</code></p>
</li>
<li>
<p><code>FormData</code></p>
</li>
<li>
<p><code>null</code></p>
</li>
</ul>
<p>如果是 GET/HEAD请求,<code>send()</code>方法一般不传参或传 <code>null</code>。不过即使你真传入了参数,参数也最终被忽略,<code>xhr.send(data)</code>中的data会被置为 <code>null</code>.</p>
<p><code>xhr.send(data)</code>中data参数的数据类型会影响请求头部<code>content-type</code>的默认值:</p>
<ul>
<li>
<p>如果<code>data</code>是 <code>Document</code> 类型,同时也是<code>HTML Document</code>类型,则<code>content-type</code>默认值为<code>text/html;charset=UTF-8</code>;否则为<code>application/xml;charset=UTF-8</code>;</p>
</li>
<li>
<p>如果<code>data</code>是 <code>DOMString</code> 类型,<code>content-type</code>默认值为<code>text/plain;charset=UTF-8</code>;</p>
</li>
<li>
<p>如果<code>data</code>是 <code>FormData</code> 类型,<code>content-type</code>默认值为<code>multipart/form-data; boundary=</code></p>
</li>
<li>
<p>如果<code>data</code>是其他类型,则不会设置<code>content-type</code>的默认值</p>
</li>
</ul>
<p>当然这些只是<code>content-type</code>的默认值,但如果用<code>xhr.setRequestHeader()</code>手动设置了中<code>content-type</code>的值,以上默认值就会被覆盖。</p>
<p>另外需要注意的是,若在断网状态下调用<code>xhr.send(data)</code>方法,则会抛错:<code>Uncaught NetworkError: Failed to execute 'send' on 'XMLHttpRequest'</code>。一旦程序抛出错误,如果不 catch 就无法继续执行后面的代码,所以调用 <code>xhr.send(data)</code>方法时,应该用 <code>try-catch</code>捕捉错误。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)">{
xhr.send(data)
}</span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)">(e) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">doSomething...</span>
};</pre>
</div>
<h3><code>10、xhr.withCredentials</code>与 <code>CORS</code> 什么关系</h3>
<blockquote>
<p>我们都知道,在发同域请求时,浏览器会将<code>cookie</code>自动加在<code>request header</code>中。但大家是否遇到过这样的场景:在发送跨域请求时,<code>cookie</code>并没有自动加在<code>request header</code>中。</p>
</blockquote>
<p>造成这个问题的原因是:在<code>CORS</code>标准中做了规定,默认情况下,浏览器在发送跨域请求时,不能发送任何认证信息(<code>credentials</code>)如"<code>cookies</code>"和"<code>HTTP authentication schemes</code>"。除非<code>xhr.withCredentials</code>为<code>true</code>(<code>xhr</code>对象有一个属性叫<code>withCredentials</code>,默认值为<code>false</code>)。</p>
<p>所以根本原因是<code>cookies</code>也是一种认证信息,在跨域请求中,<code>client</code>端必须手动设置<code>xhr.withCredentials=true</code>,且<code>server</code>端也必须允许<code>request</code>能携带认证信息(即<code>response header</code>中包含<code>Access-Control-Allow-Credentials:true</code>),这样浏览器才会自动将<code>cookie</code>加在<code>request header</code>中。</p>
<p>另外,要特别注意一点,一旦跨域<code>request</code>能够携带认证信息,<code>server</code>端一定不能将<code>Access-Control-Allow-Origin</code>设置为<code>*</code>,而必须设置为请求页面的域名。</p>
<h2><code>四、xhr</code>相关事件</h2>
<h3>1、事件分类</h3>
<p><code>xhr</code>相关事件有很多,有时记起来还挺容易混乱。但当我了解了具体代码实现后,就容易理清楚了。下面是<code>XMLHttpRequest</code>的部分实现代码:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">interface XMLHttpRequestEventTarget : EventTarget {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> event handlers</span>
<span style="color: rgba(0, 0, 0, 1)">attribute EventHandler onloadstart;
attribute EventHandler onprogress;
attribute EventHandler onabort;
attribute EventHandler onerror;
attribute EventHandler onload;
attribute EventHandler ontimeout;
attribute EventHandler onloadend;
};
interface XMLHttpRequestUpload : XMLHttpRequestEventTarget {
};
interface XMLHttpRequest : XMLHttpRequestEventTarget {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> event handler</span>
<span style="color: rgba(0, 0, 0, 1)">attribute EventHandler onreadystatechange;
readonly attribute XMLHttpRequestUpload upload;
};</span></pre>
</div>
<p>从代码中我们可以看出:</p>
<ol>
<li>
<p><code>XMLHttpRequestEventTarget</code>接口定义了7个事件:</p>
<ul>
<li>
<p><code>onloadstart</code></p>
</li>
<li>
<p><code>onprogress</code></p>
</li>
<li>
<p><code>onabort</code></p>
</li>
<li>
<p><code>ontimeout</code></p>
</li>
<li>
<p><code>onerror</code></p>
</li>
<li>
<p><code>onload</code></p>
</li>
<li>
<p><code>onloadend</code></p>
</li>
</ul>
</li>
<li>
<p>每一个<code>XMLHttpRequest</code>里面都有一个<code>upload</code>属性,而<code>upload</code>是一个<code>XMLHttpRequestUpload</code>对象</p>
</li>
<li>
<p><code>XMLHttpRequest</code>和<code>XMLHttpRequestUpload</code>都继承了同一个<code>XMLHttpRequestEventTarget</code>接口,所以<code>xhr</code>和<code>xhr.upload</code>都有第一条列举的7个事件</p>
</li>
<li>
<p><code>onreadystatechange</code>是<code>XMLHttpRequest</code>独有的事件</p>
</li>
</ol>
<p>所以这么一看就很清晰了:<br><code>xhr</code>一共有8个相关事件:7个<code>XMLHttpRequestEventTarget</code>事件+1个独有的<code>onreadystatechange</code>事件;而<code>xhr.upload</code>只有7个<code>XMLHttpRequestEventTarget</code>事件。</p>
<h3>2、事件触发条件</h3>
<p>下面是我自己整理的一张<code>xhr</code>相关事件触发条件表,其中最需要注意的是 <code>onerror</code> 事件的触发条件。</p>
<table>
<thead>
<tr><th>事件</th><th>触发条件</th></tr>
</thead>
<tbody>
<tr>
<td><code>onreadystatechange</code></td>
<td>每当<code>xhr.readyState</code>改变时触发;但<code>xhr.readyState</code>由非<code>0</code>值变为<code>0</code>时不触发。</td>
</tr>
<tr>
<td><code>onloadstart</code></td>
<td>调用<code>xhr.send()</code>方法后立即触发,若<code>xhr.send()</code>未被调用则不会触发此事件。</td>
</tr>
<tr>
<td><code>onprogress</code></td>
<td><code>xhr.upload.onprogress</code>在上传阶段(即<code>xhr.send()</code>之后,<code>xhr.readystate=2</code>之前)触发,每50ms触发一次;<code>xhr.onprogress</code>在下载阶段(即<code>xhr.readystate=3</code>时)触发,每50ms触发一次。</td>
</tr>
<tr>
<td><code>onload</code></td>
<td>当请求成功完成时触发,此时<code>xhr.readystate=4</code></td>
</tr>
<tr>
<td><code>onloadend</code></td>
<td>当请求结束(包括请求成功和请求失败)时触发</td>
</tr>
<tr>
<td><code>onabort</code></td>
<td>当调用<code>xhr.abort()</code>后触发</td>
</tr>
<tr>
<td><code>ontimeout</code></td>
<td><code>xhr.timeout</code>不等于0,由请求开始即<code>onloadstart</code>开始算起,当到达<code>xhr.timeout</code>所设置时间请求还未结束即<code>onloadend</code>,则触发此事件。</td>
</tr>
<tr>
<td><code>onerror</code></td>
<td>
<p>在请求过程中,若发生<code>Network error</code>则会触发此事件(若发生<code>Network error</code>时,上传还没有结束,则会先触发<code>xhr.upload.onerror</code>,再触发<code>xhr.onerror</code>;若发生<code>Network error</code>时,</p>
<p>上传已经结束,则只会触发<code>xhr.onerror</code>)。注意,只有发生了网络层级别的异常才会触发此事件,对于应用层级别的异常,如响应返回的<code>xhr.statusCode</code>是<code>4xx</code>时,并不属于<code>Network error</code>,</p>
<p>所以不会触发<code>onerror</code>事件,而是会触发<code>onload</code>事件。</p>
</td>
</tr>
</tbody>
</table>
<h3>3、事件触发顺序</h3>
<p>当请求一切正常时,相关的事件触发顺序如下:</p>
<ol>
<li>
<p>触发<code>xhr.onreadystatechange</code>(之后每次<code>readyState</code>变化时,都会触发一次)</p>
</li>
<li>
<p>触发<code>xhr.onloadstart</code><br>//上传阶段开始:</p>
</li>
<li>
<p>触发<code>xhr.upload.onloadstart</code></p>
</li>
<li>
<p>触发<code>xhr.upload.onprogress</code></p>
</li>
<li>
<p>触发<code>xhr.upload.onload</code></p>
</li>
<li>
<p>触发<code>xhr.upload.onloadend</code><br>//上传结束,下载阶段开始:</p>
</li>
<li>
<p>触发<code>xhr.onprogress</code></p>
</li>
<li>
<p>触发<code>xhr.onload</code></p>
</li>
<li>
<p>触发<code>xhr.onloadend</code></p>
</li>
</ol>
<h4>发生<code>abort</code>/<code>timeout</code>/<code>error</code>异常的处理</h4>
<p>在请求的过程中,有可能发生 <code>abort</code>/<code>timeout</code>/<code>error</code>这3种异常。那么一旦发生这些异常,<code>xhr</code>后续会进行哪些处理呢?后续处理如下:</p>
<ol>
<li>
<p>一旦发生<code>abort</code>或<code>timeout</code>或<code>error</code>异常,先立即中止当前请求</p>
</li>
<li>
<p>将 <code>readystate</code> 置为<code>4</code>,并触发 <code>xhr.onreadystatechange</code>事件</p>
</li>
<li>
<p>如果上传阶段还没有结束,则依次触发以下事件:</p>
<ul>
<li>
<p><code>xhr.upload.onprogress</code></p>
</li>
<li>
<p><code>xhr.upload.</code></p>
</li>
<li>
<p><code>xhr.upload.onloadend</code></p>
</li>
</ul>
</li>
<li>
<p>触发 <code>xhr.onprogress</code>事件</p>
</li>
<li>
<p>触发 <code>xhr.</code>事件</p>
</li>
<li>
<p>触发<code>xhr.onloadend</code> 事件</p>
</li>
</ol>
<h4>在哪个<code>xhr</code>事件中注册成功回调?</h4>
<p>从上面介绍的事件中,可以知道若<code>xhr</code>请求成功,就会触发<code>xhr.onreadystatechange</code>和<code>xhr.onload</code>两个事件。 那么我们到底要将成功回调注册在哪个事件中呢?我倾向于 <code>xhr.onload</code>事件,因为<code>xhr.onreadystatechange</code>是每次<code>xhr.readyState</code>变化时都会触发,而不是<code>xhr.readyState=4</code>时才触发。</p>
<div class="cnblogs_code">
<pre>xhr.onload = <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> () {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">如果请求成功</span>
<span style="color: rgba(0, 0, 255, 1)">if</span>(xhr.status == 200<span style="color: rgba(0, 0, 0, 1)">){
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">do successCallback</span>
<span style="color: rgba(0, 0, 0, 1)"> }
}</span></pre>
</div>
<p>上面的示例代码是很常见的写法:先判断<code>http</code>状态码是否是<code>200</code>,如果是,则认为请求是成功的,接着执行成功回调。这样的判断是有坑儿的,比如当返回的<code>http</code>状态码不是<code>200</code>,而是<code>201</code>时,请求虽然也是成功的,但并没有执行成功回调逻辑。所以更靠谱的判断方法应该是:当<code>http</code>状态码为<code>2xx</code>或<code>304</code>时才认为成功。</p>
<div class="cnblogs_code">
<pre>xhr.onload = <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> () {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">如果请求成功</span>
<span style="color: rgba(0, 0, 255, 1)">if</span>((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304<span style="color: rgba(0, 0, 0, 1)">){
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">do successCallback</span>
<span style="color: rgba(0, 0, 0, 1)"> }
}</span></pre>
</div>
<p>转自:https://blog.csdn.net/z550449054/article/details/80538623</p><br><br>
来源:https://www.cnblogs.com/vickylinj/p/14435521.html
頁:
[1]