你真的知道如何使用 XMLHttpRequest 吗?
看到标题时,有些同学可能会想:“我已经用
xhr
成功地发过很多个Ajax
请求了,对它的基本操作已经算挺熟练了。” 我之前的想法和你们一样,直到最近我使用xhr
时踩了不少坑儿,我才突然发现其实自己并不够了解xhr
,我知道的只是最最基本的使用。
于是我决定好好地研究一番xhr
的真面目,可拜读了不少博客后都不甚满意,于是我决定认真阅读一遍W3C的XMLHttpRequest
标准。看完标准后我如同醍醐灌顶一般,感觉到了从未有过的清澈。这篇文章就是参考W3C的XMLHttpRequest
标准和结合一些实践验证总结而来的。
<a name="t1"></a>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
是一种技术方案,但并不是一种<span style="font-weight:700">新技术</span>。它依赖的是现有的CSS
/HTML
/Javascript
,而其中最核心的依赖是浏览器提供的XMLHttpRequest
对象,是这个对象使得浏览器可以发出HTTP
请求与接收HTTP
响应。
所以我用一句话来总结两者的关系:我们使用XMLHttpRequest
对象来发送一个Ajax
请求。
<a name="t2"></a>XMLHttpRequest
的发展历程
XMLHttpRequest
一开始只是微软浏览器提供的一个接口,后来各大浏览器纷纷效仿也提供了这个接口,再后来W3C对它进行了标准化,提出了XMLHttpRequest标准。XMLHttpRequest
标准又分为Level 1
和Level 2
。XMLHttpRequest Level 1
主要存在以下缺点:
受同源策略的限制,不能发送跨域请求;
不能发送二进制文件(如图片、视频、音频等),只能发送纯文本数据;
在发送和获取数据的过程中,无法实时获取进度信息,只能判断是否完成;
那么Level 2
对Level 1
进行了改进,XMLHttpRequest Level 2
中新增了以下功能:
可以发送跨域请求,在服务端允许的情况下;
支持发送和接收二进制数据;
新增formData对象,支持发送表单数据;
发送和获取数据时,可以获取进度信息;
可以设置请求的超时时间;
当然更详细的对比介绍,可以参考阮老师的这篇文章,文章中对新增的功能都有具体代码示例。
<a name="t3"></a>XMLHttpRequest
兼容性
关于xhr
的浏览器兼容性,大家可以直接查看“Can I use”这个网站提供的结果XMLHttpRequest兼容性,下面提供一个截图。
[图片上传失败...(image-195ba8-1582612078413)]
从图中可以看到:
IE8/IE9、Opera Mini 完全不支持
xhr
对象IE10/IE11部分支持,不支持
xhr.responseType
为json
部分浏览器不支持设置请求超时,即无法使用
xhr.timeout
部分浏览器不支持
xhr.responseType
为blob
<a name="t4"></a>细说XMLHttpRequest
如何使用
先来看一段使用XMLHttpRequest
发送Ajax
请求的简单示例代码。
function sendAjax() {
//构造表单数据
var formData = new FormData();
formData.append('username', 'johndoe');
formData.append('id', 123456);
//创建xhr对象
var xhr = new XMLHttpRequest();
//设置xhr请求的超时时间
xhr.timeout = 3000;
//设置响应返回的数据格式
xhr.responseType = "text";
//创建一个 post 请求,采用异步
xhr.open('POST', '/server', true);
//注册相关事件回调处理函数
xhr.onload = function(e) {
if(this.status == 200||this.status == 304){
alert(this.responseText);
}
};
xhr.ontimeout = function(e) { ... };
xhr.onerror = function(e) { ... };
xhr.upload.onprogress = function(e) { ... };
//发送数据
xhr.send(formData);
}
上面是一个使用xhr
发送表单数据的示例,整个流程可以参考注释。
接下来我将站在使用者的角度,以问题的形式介绍
xhr
的基本使用。
我对每一个问题涉及到的知识点都会进行比较细致地介绍,有些知识点可能是你平时忽略关注的。
<a name="t5"></a>如何设置request header
在发送Ajax
请求(实质是一个HTTP请求)时,我们可能需要设置一些请求头部信息,比如content-type
、connection
、cookie
、accept-xxx
等。xhr
提供了setRequestHeader
来允许我们修改请求 header。
void setRequestHeader(DOMString header, DOMString value);
<span style="font-weight:700">注意点</span>:
方法的第一个参数 header 大小写不敏感,即可以写成
content-type
,也可以写成Content-Type
,甚至写成content-Type
;Content-Type
的默认值与具体发送的数据类型有关,请参考本文【可以发送什么类型的数据】一节;setRequestHeader
必须在open()
方法之后,send()
方法之前调用,否则会抛错;setRequestHeader
可以调用多次,最终的值不会采用覆盖override
的方式,而是采用追加append
的方式。下面是一个示例代码:
var client = new XMLHttpRequest();
client.open('GET', 'demo.cgi');
client.setRequestHeader('X-Test', 'one');
client.setRequestHeader('X-Test', 'two');
// 最终request header中"X-Test"为: one, two
client.send();
<a name="t6"></a>如何获取response header
xhr
提供了2个用来获取响应头部的方法:getAllResponseHeaders
和getResponseHeader
。前者是获取 response 中的所有header 字段,后者只是获取某个指定 header 字段的值。另外,getResponseHeader(header)
的header
参数不区分大小写。
DOMString getAllResponseHeaders();
DOMString getResponseHeader(DOMString header);
<span style="">这2个方法看起来简单,但却处处是坑儿。</span>
你是否遇到过下面的坑儿?——反正我是遇到了。。。
使用
getAllResponseHeaders()
看到的所有response header
与实际在控制台Network
中看到的response header
不一样使用
getResponseHeader()
获取某个header
的值时,浏览器抛错Refused to get unsafe header "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
"进行<span style="font-weight:700">跨域请求</span>时响应头部中的一个字段,对于同域请求,响应头部是没有这个字段的。这个字段中列举的 header 字段就是服务器允许暴露给客户端访问的字段。
所以getAllResponseHeaders()
只能拿到<span style="font-weight:700"><span style="">限制以外</span></span>(即被视为safe
)的header字段,而不是全部字段;而调用getResponseHeader(header)
方法时,header
参数必须是<span style="font-weight:700"><span style="">限制以外</span></span>的header字段,否则调用就会报Refused to get unsafe header
的错误。
<a name="t7"></a>如何指定xhr.response
的数据类型
有些时候我们希望xhr.response
返回的就是我们想要的数据类型。比如:响应返回的数据是纯JSON字符串,但我们期望最终通过xhr.response
拿到的直接就是一个 js 对象,我们该怎么实现呢?
有2种方法可以实现,一个是level 1
就提供的overrideMimeType()
方法,另一个是level 2
才提供的xhr.responseType
属性。
<a name="t8"></a>xhr.overrideMimeType()
overrideMimeType
是xhr level 1
就有的方法,所以浏览器兼容性良好。这个方法的作用就是用来重写response
的content-type
,这样做有什么意义呢?比如:server 端给客户端返回了一份document
或者是 xml
文档,我们希望最终通过xhr.response
拿到的就是一个DOM
对象,那么就可以用xhr.overrideMimeType('text/xml; charset = utf-8')
来实现。
再举一个使用场景,我们都知道xhr level 1
不支持直接传输blob二进制数据,那如果真要传输 blob 该怎么办呢?当时就是利用overrideMimeType
方法来解决这个问题的。
下面是一个获取图片文件的代码示例:
var xhr = new XMLHttpRequest();
//向 server 端获取一张图片
xhr.open('GET', '/path/to/image.png', true);
// 这行是关键!
//将响应数据按照纯文本格式来解析,字符集替换为用户自己定义的字符集
xhr.overrideMimeType('text/plain; charset=x-user-defined');
xhr.onreadystatechange = function(e) {
if (this.readyState == 4 && this.status == 200) {
//通过 responseText 来获取图片文件对应的二进制字符串
var binStr = this.responseText;
//然后自己再想方法将逐个字节还原为二进制数据
for (var i = 0, len = binStr.length; i < len; ++i) {
var c = binStr.charCodeAt(i);
//String.fromCharCode(c & 0xff);
var byte = c & 0xff;
}
}
};
xhr.send();
代码示例中xhr
请求的是一张图片,通过将 response
的 content-type
改为'text/plain; charset=x-user-defined',使得 xhr
以纯文本格式来解析接收到的blob 数据,最终用户通过this.responseText
拿到的就是图片文件对应的二进制字符串,最后再将其转换为 blob 数据。
<a name="t9"></a>xhr.responseType
responseType
是xhr level 2
新增的属性,用来指定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
来实现简单得多。
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
//可以将`xhr.responseType`设置为`"blob"`也可以设置为`" arrayBuffer"`
//xhr.responseType = 'arrayBuffer';
xhr.responseType = 'blob';
xhr.onload = function(e) {
if (this.status == 200) {
var blob = this.response;
...
}
};
xhr.send();
<a name="t10"></a>小结
虽然在xhr level 2
中,2者是共同存在的。但其实不难发现,xhr.responseType
就是用来取代xhr.overrideMimeType()
的,xhr.responseType
功能强大的多,xhr.overrideMimeType()
能做到的xhr.responseType
都能做到。所以我们现在完全可以摒弃使用xhr.overrideMimeType()
了。
<a name="t11"></a>如何获取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
:请求未完成、请求失败、请求成功但返回数据无法被正确解析时
<a name="t12"></a>如何追踪ajax
请求的当前状态
在发一个ajax
请求后,如果想追踪请求当前处于哪种状态,该怎么做呢?
用xhr.readyState
这个属性即可追踪到。这个属性是只读属性,总共有5种可能值,分别对应xhr
不同的不同阶段。每次xhr.readyState
的值发生变化时,都会触发xhr.onreadystatechange
事件,我们可以在这个事件中进行相关状态判断。
xhr.onreadystatechange = function () {
switch(xhr.readyState){
case 1://OPENED
//do something
break;
case 2://HEADERS_RECEIVED
//do something
break;
case 3://LOADING
//do something
break;
case 4://DONE
//do something
break;
}
值 | 状态 | 描述 |
---|---|---|
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 (整个数据传输过程结束) |
整个数据传输过程结束,不管本次请求是成功还是失败 |
<a name="t13"></a>如何设置请求的超时时间
如果请求过了很久还没有成功,为了不会白白占用的网络资源,我们一般会主动终止请求。XMLHttpRequest
提供了timeout
属性来允许设置请求的超时时间。
xhr.timeout
单位:milliseconds 毫秒
默认值:0
,即不设置超时
很多同学都知道:从<span style="font-weight:700"><span style="">请求开始</span></span> 算起,若超过 timeout
时间请求还没有结束(包括成功/失败),则会触发ontimeout事件,主动结束该请求。
【那么到底什么时候才算是<span style="font-weight:700"><span style="">请求开始</span></span> ?】
——xhr.onloadstart
事件触发的时候,也就是你调用xhr.send()
方法的时候。
因为xhr.open()
只是创建了一个连接,但并没有真正开始数据的传输,而xhr.send()
才是真正开始了数据的传输过程。只有调用了xhr.send()
,才会触发xhr.onloadstart
。
【那么什么时候才算是<span style="font-weight:700"><span style="">请求结束</span></span> ?】
—— xhr.loadend
事件触发的时候。
另外,还有2个需要注意的坑儿:
可以在
send()
之后再设置此xhr.timeout
,但计时起始点仍为调用xhr.send()
方法的时刻。当
xhr
为一个sync
同步请求时,xhr.timeout
必须置为0
,否则会抛错。原因可以参考本文的【如何发一个同步请求】一节。
<a name="t14"></a>如何发一个同步请求
xhr
默认发的是异步请求,但也支持发同步请求(当然实际开发中应该尽量避免使用)。到底是异步还是同步请求,由xhr.open()
传入的async
参数决定。
open(method, url [, async = true [, username = null [, password = null]]])
method
: 请求的方式,如GET/POST/HEADER
等,这个参数不区分大小写url
: 请求的地址,可以是相对地址如example.[PHP](http://lib.****.net/base/php "PHP知识库")
,这个<span style="font-weight:700">相对</span>是相对于当前网页的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
事件也不会触发。
<a name="t15"></a>如何获取上传、下载的进度
在上传或者下载比较大的文件时,实时显示当前的上传、下载进度是很普遍的产品需求。
我们可以通过onprogress
事件来实时显示进度,默认情况下这个事件每50ms触发一次。需要注意的是,上传过程和下载过程触发的是不同对象的onprogress
事件:
上传触发的是
xhr.upload
对象的onprogress
事件下载触发的是
xhr
对象的onprogress
事件
xhr.onprogress = updateProgress;
xhr.upload.onprogress = updateProgress;
function updateProgress(event) {
if (event.lengthComputable) {
var completedPercent = event.loaded / event.total;
}
}
<a name="t16"></a>可以发送什么类型的数据
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
类型,同时也是HTML Document
类型,则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)
方法,则会抛错:Uncaught NetworkError: Failed to execute 'send' on 'XMLHttpRequest'
。一旦程序抛出错误,如果不 catch 就无法继续执行后面的代码,所以调用 xhr.send(data)
方法时,应该用 try-catch
捕捉错误。
try{
xhr.send(data)
}catch(e) {
//doSomething...
};
<a name="t17"></a>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
设置为*
,否则就会面临攻击危险。
<a name="t18"></a>xhr
相关事件
<a name="t19"></a>事件分类
xhr
相关事件有很多,有时记起来还挺容易混乱。但当我了解了具体代码实现后,就容易理清楚了。下面是XMLHttpRequest
的部分实现代码:
interface XMLHttpRequestEventTarget : EventTarget {
// event handlers
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 {
// event handler
attribute EventHandler onreadystatechange;
readonly attribute XMLHttpRequestUpload upload;
};
从代码中我们可以看出:
-
XMLHttpRequestEventTarget
接口定义了7个事件:onloadstart
onprogress
onabort
ontimeout
onerror
onload
onloadend
每一个
XMLHttpRequest
里面都有一个upload
属性,而upload
是一个XMLHttpRequestUpload
对象XMLHttpRequest
和XMLHttpRequestUpload
都继承了同一个XMLHttpRequestEventTarget
接口,所以xhr
和xhr.upload
都有第一条列举的7个事件onreadystatechange
是XMLHttpRequest
独有的事件
所以这么一看就很清晰了:xhr
一共有8个相关事件:7个XMLHttpRequestEventTarget
事件+1个独有的onreadystatechange
事件;而xhr.upload
只有7个XMLHttpRequestEventTarget
事件。
<a name="t20"></a>事件触发条件
下面是我自己整理的一张xhr
相关事件触发条件表,其中最需要注意的是 onerror
事件的触发条件。
事件 | 触发条件 |
---|---|
onreadystatechange |
每当xhr.readyState 改变时触发;但xhr.readyState 由非0 值变为0 时不触发。 |
onloadstart |
调用xhr.send() 方法后立即触发,若xhr.send() 未被调用则不会触发此事件。 |
onprogress |
xhr.upload.onprogress 在上传阶段(即xhr.send() 之后,xhr.readystate=2 之前)触发,每50ms触发一次;xhr.onprogress 在下载阶段(即xhr.readystate=3 时)触发,每50ms触发一次。 |
onload |
当请求成功完成时触发,此时xhr.readystate=4
|
onloadend |
当请求结束(包括请求成功和请求失败)时触发 |
onabort |
当调用xhr.abort() 后触发 |
ontimeout |
xhr.timeout 不等于0,由请求开始即onloadstart 开始算起,当到达xhr.timeout 所设置时间请求还未结束即onloadend ,则触发此事件。 |
onerror |
在请求过程中,若发生Network error 则会触发此事件(若发生Network error 时,上传还没有结束,则会先触发xhr.upload.onerror ,再触发xhr.onerror ;若发生Network error 时,上传已经结束,则只会触发xhr.onerror )。<span style="font-weight:700">注意</span>,只有发生了网络层级别的异常才会触发此事件,对于应用层级别的异常,如响应返回的xhr.statusCode 是4xx 时,并不属于Network error ,所以不会触发onerror 事件,而是会触发onload 事件。 |
<a name="t21"></a>事件触发顺序
当请求一切正常时,相关的事件触发顺序如下:
触发
xhr.onreadystatechange
(之后每次readyState
变化时,都会触发一次)触发
xhr.onloadstart
//上传阶段开始:触发
xhr.upload.onloadstart
触发
xhr.upload.onprogress
触发
xhr.upload.onload
触发
xhr.upload.onloadend
//上传结束,下载阶段开始:触发
xhr.onprogress
触发
xhr.onload
触发
xhr.onloadend
<a name="t22"></a>发生abort
/timeout
/error
异常的处理
在请求的过程中,有可能发生 abort
/timeout
/error
这3种异常。那么一旦发生这些异常,xhr
后续会进行哪些处理呢?后续处理如下:
一旦发生
abort
或timeout
或error
异常,先立即中止当前请求将
readystate
置为4
,并触发xhr.onreadystatechange
事件-
如果上传阶段还没有结束,则依次触发以下事件:
xhr.upload.onprogress
xhr.upload.[onabort或ontimeout或onerror]
xhr.upload.onloadend
触发
xhr.onprogress
事件触发
xhr.[onabort或ontimeout或onerror]
事件触发
xhr.onloadend
事件
<a name="t23"></a>在哪个xhr
事件中注册成功回调?
从上面介绍的事件中,可以知道若xhr
请求成功,就会触发xhr.onreadystatechange
和xhr.onload
两个事件。 那么我们到底要将成功回调注册在哪个事件中呢?我倾向于 xhr.onload
事件,因为xhr.onreadystatechange
是每次xhr.readyState
变化时都会触发,而不是xhr.readyState=4
时才触发。
xhr.onload = function () {
//如果请求成功
if(xhr.status == 200){
//do successCallback
}
}
上面的示例代码是很常见的写法:先判断http
状态码是否是200
,如果是,则认为请求是成功的,接着执行成功回调。这样的判断是有坑儿的,比如当返回的http
状态码不是200
,而是201
时,请求虽然也是成功的,但并没有执行成功回调逻辑。所以更靠谱的判断方法应该是:当http
状态码为2xx
或304
时才认为成功。
xhr.onload = function () {
//如果请求成功
if((xhr.status >= 200 && xhr.status <= 200) || xhr.status == 304){
//do successCallback
}
}
本文转自:segmentfault
结语
最后给点扩展学习资料,如果你:
想真正搞懂
XMLHttpRequest
,最靠谱的方法还是看 W3C的xhr 标准;想结合代码学习如何用
XMLHttpRequest
发各种类型的数据,可以参考html5rocks上的这篇文章想粗略的了解
XMLHttpRequest
的基本使用,可以参考MDN的XMLHttpRequest介绍;想了解
XMLHttpRequest
的发展历程,可以参考阮老师的文章;想了解
Ajax
的基本介绍,可以参考AJAX Tutorial;想了解跨域请求,则可以参考W3C的 cors 标准;
想了解
http
协议,则可以参考HTTP Tutorial;
上一篇: html - 旋转眩晕效果
推荐阅读
-
你真的需要 MacBook 吗?如何在 Windows 和 Mac OS 之间做出选择?
-
[你真的知道如何使用 ES 吗?
-
手把手教你如何注册谷歌账号--这个手机号不能用来验证。于是人们想当然地认为,谷歌退出的原因,并不是对国内手机号验证通过。其实不然,根据谷歌的注册机制,这是注册时的第一次验证,也属于机器验证,即判断注册者是否为机器人,以防止滥用,所以这次验证只是纯粹的手机号验证,并不是绑定手机号。 那么我们就有疑问了,第一次验证要求手机号验证却显示手机号无法验证,这不是自相矛盾吗? 谷歌现在在全球至少有14亿注册用户,所以谷歌对注册的网络、机器要求非常严格,可以说是判断筛选机制,你的网络、机器或者浏览器身份验证,不符合谷歌机器认证,就不允许你验证通过,而不是因为手机号的问题。不信的朋友可以换个美国手机号输入试试,同样显示这个手机号无法验证。 所以我不建议使用代理网络注册,因为那是一个共享通道,已经被滥用了,这个时候遇到无法验证也就不奇怪了,即使有些朋友侥幸通过了验证,这个号码也活不了多久,因为很快就会被检测出是什么ip注册的,然后出现异常验证,甚至直接封号,这个时候你就后悔晚矣。所以对于QQ邮箱谷歌注册入口,笔者强烈不推荐这样做,因为QQ邮箱注册早就被滥用了,会有那么多人会遇到异常验证的情况。 异常验证在你输入任何手机号码的情况下都会显示无法验证或者手机号码验证次数过多。
-
你真的知道如何使用 XMLHttpRequest 吗?
-
你真的知道如何使用 XMLHttpRequest 吗?
-
Java 中的 BigDecimal,您真的知道如何使用它吗?
-
epoll简介及触发模式(accept、read、send)-epoll的简单介绍 epoll在LT和ET模式下的读写方式 一、epoll的接口非常简单,一共就三个函数:1. int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close关闭,否则可能导致fd被耗尽。2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll的事件注册函数,它不同与select是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create的返回值,第二个参数表示动作,用三个宏来表示:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};events可以是以下几个宏的集合:EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLIN事件:EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。现在明白为什么说epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会在堵死在阻塞里。 EPOLLOUT:表示对应的文件描述符可以写; EPOLLOUT事件:EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那要先准备好下面条件:1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!其实,如果真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。1. 缓冲区由满变空.2.同时注册EPOLLIN | EPOLLOUT事件,也会触发一次EPOLLOUT事件这个两个也会触发EPOLLOUT事件 EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的产生,类似于select调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。-------------------------------------------------------------------------------------------- 从man手册中,得到ET和LT的具体描述如下EPOLL事件有两种模型:Edge Triggered (ET)Level Triggered (LT)假如有这样一个例子:1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符2. 这个时候从管道的另一端被写入了2KB的数据3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作4. 然后我们读取了1KB的数据5. 调用epoll_wait(2)......Edge Triggered 工作模式:如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。 i 基于非阻塞文件句柄 ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。Level Triggered 工作模式相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。然后详细解释ET, LT:LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取: 这里只是说明思路(参考《UNIX网络编程》) while(rs) {buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno == EAGAIN)break; else return; }else if(buflen == 0) { // 这里表示对端的socket已正常关闭. } if(buflen == sizeof(buf) rs = 1; // 需要再次读取 else rs = 0; } 还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send内部,当写缓冲已满(send返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send内部,但暂没有更好的办法. ssize_t socket_send(int sockfd, const char* buffer, size_t buflen) { ssize_t tmp; size_t total = buflen; const char *p = buffer; while(1) { tmp = send(sockfd, p, total, 0); if(tmp < 0) { // 当send收到信号时,可以继续写,但这里返回-1. if(errno == EINTR) return -1; // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满, // 在这里做延时后再重试. if(errno == EAGAIN) { usleep(1000); continue; } return -1; } if((size_t)tmp == total) return buflen; total -= tmp; p += tmp; } return tmp; } 二、epoll在LT和ET模式下的读写方式 在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK) 从字面上看, 意思是: * EAGAIN: 再试一次 * EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block * perror输出: Resource temporarily unavailable 总结: 这个错误表示资源暂时不够, 可能read时, 读缓冲区没有数据, 或者, write时,写缓冲区满了 。 遇到这种情况, 如果是阻塞socket, read/write就要阻塞掉。 而如果是非阻塞socket, read/write立即返回-1, 同 时errno设置为EAGAIN. 所以, 对于阻塞socket, read/write返回-1代表网络出错了. 但对于非阻塞socket, read/write返回-1不一定网络真的出错了. 可能是Resource temporarily unavailable. 这时你应该再试, 直到Resource available. 综上, 对于non-blocking的socket, 正确的读写操作为: 读: 忽略掉errno = EAGAIN的错误, 下次继续读 写: 忽略掉errno = EAGAIN的错误, 下次继续写 对于select和epoll的LT模式, 这种读写方式是没有问题的. 但对于epoll的ET模式, 这种方式还有漏洞. epoll的两种模式 LT 和 ET
-
死鬼,你真的知道如何编写单元测试吗?
-
最好的 Docker 可视化管理工具 - Portainer 你真的知道如何使用它吗?
-
纯干货分享 | 研发效能提升——敏捷需求篇-而敏捷需求是提升效能的方式中不可或缺的模块之一。 云智慧的敏捷教练——Iris Xu近期在公司做了一场分享,主题为「敏捷需求挖掘和组织方法,交付更高业务价值的产品」。Iris具有丰富的团队敏捷转型实施经验,完成了企业多个团队从传统模式到敏捷转型的落地和实施,积淀了很多的经验。 这次分享主要包含以下2个部分: 第一部分是用户影响地图 第二部分是事件驱动的业务分析Event driven business analysis(以下简称EDBA) 用户影响地图,是一种从业务目标到产品需求映射的需求挖掘和组织的方法。 在软件开发过程中可能会遇到一些问题,比如大家使用不同的业务语言、技术语言,造成角色间的沟通阻碍,还会导致一些问题,比如需求误解、需求传递错误等;这会直接导致产品的功能需求和要实现的业务目标不是映射关系。 但在交付期间,研发人员必须要将这些需求实现交付,他们实则并不清楚这些功能需求产生的原因是什么、要解决客户的哪些痛点。研发人员往往只是拿到了解决方案,需要把它实现,但没有和业务侧一起去思考解决方案是否正确,能否真正的帮助客户解决问题。而用户影响地图通常是能够连接业务目标和产品功能的一种手段。 我们在每次迭代里加入的假设,也就是功能需求。首先把它先实现,再逐步去验证我们每一个小目标是否已经实现,再看下一个目标要是什么。那影响地图就是在这个过程中帮我们不断地去梳理目标和功能之间的关系。 我们在软件开发中可能存在的一些问题 针对这些问题,我们如何避免?先简单介绍做敏捷转型的常规思路: 先做团队级的敏捷,首先把产品、开发、测试人员,还有一些更后端的人员比如交互运维的同学放在一起,组成一个特训团队做交付。这个团队要包含交付过程中所涉及的所有角色。 接着业务敏捷要打通整个业务环节和研发侧的一个交付。上图中可以看到在敏捷中需求是分层管理的,第一层是业务需求,在这个层级是以用户目标和业务目标作为输入进行规划,同时需要去考虑客户的诉求。业务人员通过获取到的业务需求,进一步的和团队一起将其分解为产品需求。所以业务需求其实是我们真正去发布和运营的单元,它可以被独立发布到我们的生产环境上。我们的产品需求其实就是产品的具体功能,它是我们集成和测试的对象,也就是我们最终去部署到系统上的一个基本单元。产品需求再到了我们的开发团队,映射到迭代计划会上要把它分解为相应的技术任务,包括我们平时所说的比如一些前端的开发、后端的开发、测试都是相应的技术任务。所以业务敏捷要达到的目标是需要去持续顺畅高质量的交付业务价值。 将这几个点串起来,形成金字塔结构。最上层我们会把业务目标放在整个金字塔的塔尖。这个业务目标是通过用户的目标以及北极星指标确立的。确认业务目标后再去梳理相应的业务流程,最后生产。另外产品需求包含了操作流程和业务规则,具需求交付时间、工程时间以及我们的一些质量标准的要求。 谈到用户影响的地图,在敏捷江湖上其实有一个传说,大家都有一个说法叫做敏捷需求的“任督二脉”。用户影响地图其实就是任脉,在黑客马拉松上用过的用户故事地图其实叫督脉。所以说用户影响地图是在用户故事地图之前,先帮我们去梳理出我们要做哪些东西。当我们真正识别出我们要实现的业务活动之后,用户故事地图才去梳理我们整个的业务工作流,以及每个工作流节点下所要包含的具体功能和用户故事。所以说用户影响地图需要解决的问题,我们包括以下这些: 首先是范围蔓延,我们在整张地图上,功能和对应的业务目标是要去有一个映射的。这就避免了一些在我们比如有很多干系人参与的会议上,那大家都有不同想法些立场,会提出很多需求(正确以及错误的需求)。这个时候我们会依据目标去看这些需求是否真的是会影响我们的目标。 这里提到的错误需求,比如是利益相关的人提出的、客户认为产品应该有的、某个产品经理需求分析师认为可以有的....但是这些功能在用户影响地图中匹配不到对应目标的话,就需要降低优先级或弃掉。另外,通常我们去制定解决方案的时候,会考虑较完美的实现,导致解决方案括很多的功能。这个时候关键目标至关重要,会帮助我们梳理筛选、确定优先级。 看一下用户影响到地图概貌 总共分为一个三层的结构: 第一层why,你的业务目标哪个是最重要的,为什么?涉及到的角色有哪些? 第二层how ,怎样产生影响?影响用户角色什么样的行为? (不需要去列出所有的影响,基于业务目标) 第三层what,最关键的是在梳理需求时不需一次把所有细节想全,这通常团队中经常遇到的问题。 我们用这个例子来看一下 这是一个客服中心的影响地图,业务目标是 3个月内不增加客服人数的前提下能支持1.5倍的用户数。此业务目标设定是符合 smart 原则的,specific非常的具体,miserable 是可以衡量的,action reoriented是面向活动的, real list 也是很实际的。 量化的目标会指引我们接下来的行动,梳理一个业务目标,尽量去量化,比如 :我们通过打造一条什么样的流水线,能够提高整个部署的效率,时间是原来的 1/2 。这样才是一个能量化的有意义的目标。 回到这幅图, how 层级识别出来的内容,客服角色:想要对它施加的影响,把客户引导到论坛上,帮助客户更容易的跟踪问题,更快速的去定位问题。初级用户:方论坛上找到问题。高级用户:在论坛上回答问题。通过我们这些用户角色,进行活动,完成在不增加客户客服人数的前提下支持更多的用户数量。 最后一个层级,才是我们日常接触比较多的真正的功能的特性和需求,比如引导到客户到论坛上,其实这个产品就需要有一个常见问题的论坛的链接。这个层次需要我们团队进一步地在交付,在每个迭代之前做进一步的梳理,细化成相应的用户故事。 这个是云智慧团队中,自己做的影响地图的范例,可以看下整个的层级结构。序号表示优先级。 那我们用户影响地图可以总结为: