本文为转载文章,因见猎心喜,担心失传,故贴此以备不时之需。
原文地址:传送
- 推荐 23 推荐
- 收藏 159 收藏,6.6k 浏览
Ajax
和XMLHttpRequest
我们通常将Ajax
等同于XMLHttpRequest
,但细究起来它们两个是属于不同维度的2个概念。
从上面的解释中可以知道:ajax
是一种技术方案,但并不是一种新技术。它依赖的是现有的CSS
/HTML
/Javascript
,而其中最核心的依赖是浏览器提供的XMLHttpRequest
对象,是这个对象使得浏览器可以发出HTTP
请求与接收HTTP
响应。
所以我用一句话来总结两者的关系:我们使用XMLHttpRequest
对象来发送一个Ajax
请求。
XMLHttpRequest
的发展历程
XMLHttpRequest
一开始只是微软浏览器提供的一个接口,后来各大浏览器纷纷效仿也提供了这个接口,再后来W3C对它进行了标准化,提出了XMLHttpRequest
标准。XMLHttpRequest
标准又分为Level
和
1Level 2
。XMLHttpRequest Level 1
主要存在以下缺点:
受同源策略的限制,不能发送跨域请求;
不能发送二进制文件(如图片、视频、音频等),只能发送纯文本数据;
在发送和获取数据的过程中,无法实时获取进度信息,只能判断是否完成;
那么Level 2
对Level
进行了改进,
1XMLHttpRequest Level 2
中新增了以下功能:
可以发送跨域请求,在服务端允许的情况下;
支持发送和接收二进制数据;
新增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
请求的简单示例代码。
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
发送表单数据的示例,整个流程可以参考注释。
如何设置request header
在发送Ajax
请求(实质是一个HTTP请求)时,我们可能需要设置一些请求头部信息,比如content-type
、connection
、cookie
、accept-xxx
等。xhr
提供了setRequestHeader
来允许我们修改请求
header。
注意点:
方法的第一个参数 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();
如何获取response header
xhr
提供了2个用来获取响应头部的方法:getAllResponseHeaders
和getResponseHeader
。前者是获取
response 中的所有header 字段,后者只是获取某个指定 header 字段的值。另外,getResponseHeader(header)
的header
参数不区分大小写。
这2个方法看起来简单,但却处处是坑儿。
你是否遇到过下面的坑儿?——反正我是遇到了。。。
使用
getAllResponseHeaders()
看到的所有response
与实际在控制台
headerNetwork
中看到的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
”和“
headerAccess-Control-Expose-Headers
” (两个名词的解释见下方)。
所以getAllResponseHeaders()
只能拿到限制以外(即被视为safe
)的header字段,而不是全部字段;而调用getResponseHeader(header)
方法时,header
参数必须是限制以外的header字段,否则调用就会报Refused
的错误。
to get unsafe header
如何指定xhr.response
的数据类型
有些时候我们希望xhr.response
返回的就是我们想要的数据类型。比如:响应返回的数据是纯JSON字符串,但我们期望最终通过xhr.response
拿到的直接就是一个
js 对象,我们该怎么实现呢?
有2种方法可以实现,一个是level 1
就提供的overrideMimeType()
方法,另一个是level
才提供的
2xhr.responseType
属性。
xhr.overrideMimeType()
overrideMimeType
是xhr
就有的方法,所以浏览器兼容性良好。这个方法的作用就是用来重写
level 1response
的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 数据。
xhr.responseType
responseType
是xhr
新增的属性,用来指定
level 2xhr.response
的数据类型,目前还存在些兼容性问题,可以参考本文的【XMLHttpRequest
的兼容性】这一小节。那么responseType
可以设置为哪些格式呢,我简单做了一个表,如下:
"" | 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();
小结
虽然在xhr level 2
中,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
事件,我们可以在这个事件中进行相关状态判断。
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 (整个数据传输过程结束) | 整个数据传输过程结束,不管本次请求是成功还是失败 |
如何设置请求的超时时间
如果请求过了很久还没有成功,为了不会白白占用的网络资源,我们一般会主动终止请求。XMLHttpRequest
提供了timeout
属性来允许设置请求的超时时间。
单位:milliseconds 毫秒
默认值:0
,即不设置超时
很多同学都知道:从请求开始 算起,若超过 timeout
时间请求还没有结束(包括成功/失败),则会触发ontimeout事件,主动结束该请求。
【那么到底什么时候才算是请求开始 ?】
——xhr.onloadstart
事件触发的时候,也就是你调用xhr.send()
方法的时候。
因为xhr.open()
只是创建了一个连接,但并没有真正开始数据的传输,而xhr.send()
才是真正开始了数据的传输过程。只有调用了xhr.send()
,才会触发xhr.onloadstart
。
【那么什么时候才算是请求结束 ?】
—— xhr.loadend
事件触发的时候。
另外,还有2个需要注意的坑儿:
可以在
send()
之后再设置此xhr.timeout
,但计时起始点仍为调用xhr.send()
方法的时刻。当
xhr
为一个sync
同步请求时,xhr.timeout
必须置为0
,否则会抛错。原因可以参考本文的【如何发一个同步请求】一节。
如何发一个同步请求
xhr
默认发的是异步请求,但也支持发同步请求(当然实际开发中应该尽量避免使用)。到底是异步还是同步请求,由xhr.open()
传入的async
参数决定。
method
: 请求的方式,如GET/POST/HEADER
等,这个参数不区分大小写url
: 请求的地址,可以是相对地址如example.php
,这个相对是相对于当前网页的url
路径;也可以是绝对地址如http://www.example.com/example.php
async
: 默认值为true
,即为异步请求,若async=false
,则为同步请求
在我认真研读W3C 的 xhr 标准前,我总以为同步请求和异步请求只是阻塞和非阻塞的区别,其他什么事件触发、参数设置应该是一样的,事实证明我错了。
W3C 的 xhr标准中关于open()
方法有这样一段说明:
从上面一段说明可以知道,当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
事件
xhr.onprogress = updateProgress;
xhr.upload.onprogress = updateProgress;
function updateProgress(event) {
if (event.lengthComputable) {
var completedPercent = event.loaded / event.total;
}
}
可以发送什么类型的数据
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
类型,则
Documentcontent-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
。一旦程序抛出错误,如果不 catch 就无法继续执行后面的代码,所以调用
NetworkError: Failed to execute 'send' on 'XMLHttpRequest'xhr.send(data)
方法时,应该用try-catch
捕捉错误。
try{
xhr.send(data)
}catch(e) {
//doSomething...
};
xhr.withCredentials
与 CORS
什么关系
造成这个问题的原因是:在CORS
标准中做了规定,默认情况下,浏览器在发送跨域请求时,不能发送任何认证信息(credentials
)如"cookies
"和"HTTP
"。除非
authentication schemesxhr.withCredentials
为true
(xhr
对象有一个属性叫withCredentials
,默认值为false
)。
所以根本原因是cookies
也是一种认证信息,在跨域请求中,client
端必须手动设置xhr.withCredentials=true
,且server
端也必须允许request
能携带认证信息(即response
中包含
headerAccess-Control-Allow-Credentials:true
),这样浏览器才会自动将cookie
加在request
中。
header
另外,要特别注意一点,一旦跨域request
能够携带认证信息,server
端一定不能将Access-Control-Allow-Origin
设置为*
,否则就会面临攻击危险。
xhr
相关事件
事件分类
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
事件。
事件触发条件
下面是我自己整理的一张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 时,上传还没有结束,则会先触发xhr.upload.onerror ,再触发xhr.onerror ;若发生Network 时,上传已经结束,则只会触发xhr.onerror )。注意,只有发生了网络层级别的异常才会触发此事件,对于应用层级别的异常,如响应返回的xhr.statusCode 是4xx 时,并不属于Network ,所以不会触发onerror 事件,而是会触发onload 事件。 |
事件触发顺序
当请求一切正常时,相关的事件触发顺序如下:
触发
xhr.onreadystatechange
(之后每次readyState
变化时,都会触发一次)触发
xhr.onloadstart
//上传阶段开始:触发
xhr.upload.onloadstart
触发
xhr.upload.onprogress
触发
xhr.upload.onload
触发
xhr.upload.onloadend
//上传结束,下载阶段开始:触发
xhr.onprogress
触发
xhr.onload
触发
xhr.onloadend
发生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
事件
在哪个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
}
}
结语
终于写完了......
看完那一篇长长的W3C的xhr 标准,我眼睛都花了......
希望这篇总结能帮助刚开始接触XMLHttpRequest
的你。
最后给点扩展学习资料,如果你:
想真正搞懂
XMLHttpRequest
,最靠谱的方法还是看 W3C的xhr
标准;想结合代码学习如何用
XMLHttpRequest
发各种类型的数据,可以参考html5rocks上的这篇文章想粗略的了解
XMLHttpRequest
的基本使用,可以参考MDN的XMLHttpRequest介绍;想了解
XMLHttpRequest
的发展历程,可以参考阮老师的文章;想了解
Ajax
的基本介绍,可以参考AJAX
Tutorial;想了解跨域请求,则可以参考W3C的 cors 标准;
想了解
http
协议,则可以参考HTTP
Tutorial;