XMLHttpRequest

Ajax和XMLHttpRequest

我們通常將Ajax等同于XMLHttpRequest,但細(xì)究起來(lái)它們兩個(gè)是屬于不同維度的2個(gè)概念。

以下是我認(rèn)為對(duì)Ajax較為準(zhǔn)確的解釋:(摘自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是一種技術(shù)方案,但并不是一種新技術(shù)。它依賴的是現(xiàn)有的CSS/HTML/Javascript,而其中最核心的依賴是瀏覽器提供的XMLHttpRequest對(duì)象,是這個(gè)對(duì)象使得瀏覽器可以發(fā)出HTTP請(qǐng)求與接收HTTP響應(yīng)。

所以我用一句話來(lái)總結(jié)兩者的關(guān)系:我們使用XMLHttpRequest對(duì)象來(lái)發(fā)送一個(gè)Ajax請(qǐng)求。

XMLHttpRequest的發(fā)展歷程

XMLHttpRequest一開始只是微軟瀏覽器提供的一個(gè)接口,后來(lái)各大瀏覽器紛紛效仿也提供了這個(gè)接口,再后來(lái)W3C對(duì)它進(jìn)行了標(biāo)準(zhǔn)化,提出了XMLHttpRequest標(biāo)準(zhǔn)。XMLHttpRequest標(biāo)準(zhǔn)又分為L(zhǎng)evel 1和Level 2。

XMLHttpRequest Level 1主要存在以下缺點(diǎn):

受同源策略的限制,不能發(fā)送跨域請(qǐng)求;

不能發(fā)送二進(jìn)制文件(如圖片、視頻、音頻等),只能發(fā)送純文本數(shù)據(jù);

在發(fā)送和獲取數(shù)據(jù)的過(guò)程中,無(wú)法實(shí)時(shí)獲取進(jìn)度信息,只能判斷是否完成;

那么Level 2對(duì)Level 1進(jìn)行了改進(jìn),XMLHttpRequest Level 2中新增了以下功能:

可以發(fā)送跨域請(qǐng)求,在服務(wù)端允許的情況下

支持發(fā)送和接收二進(jìn)制數(shù)據(jù);

新增formData對(duì)象,支持發(fā)送表單數(shù)據(jù);

發(fā)送和獲取數(shù)據(jù)時(shí),可以獲取進(jìn)度信息;

可以設(shè)置請(qǐng)求的超時(shí)時(shí)間;

細(xì)說(shuō)XMLHttpRequest如何使用

先來(lái)看一段使用XMLHttpRequest發(fā)送Ajax請(qǐng)求的簡(jiǎn)單示例代碼。

function sendAjax() {

//構(gòu)造表單數(shù)據(jù)

var formData = new FormData();

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

formData.append('id', 123456);

//創(chuàng)建xhr對(duì)象

var xhr = new XMLHttpRequest();

//設(shè)置xhr請(qǐng)求的超時(shí)時(shí)間

xhr.timeout = 3000;

//設(shè)置響應(yīng)返回的數(shù)據(jù)格式

xhr.responseType = "text";

//創(chuàng)建一個(gè) post 請(qǐng)求,采用異步

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

//注冊(cè)相關(guān)事件回調(diào)處理函數(shù)

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) { ... };

//發(fā)送數(shù)據(jù)

xhr.send(formData);

}

上面是一個(gè)使用xhr發(fā)送表單數(shù)據(jù)的示例,整個(gè)流程可以參考注釋。

接下來(lái)我將站在使用者的角度,以問(wèn)題的形式介紹xhr的基本使用。

我對(duì)每一個(gè)問(wèn)題涉及到的知識(shí)點(diǎn)都會(huì)進(jìn)行比較細(xì)致地介紹,有些知識(shí)點(diǎn)可能是你平時(shí)忽略關(guān)注的。

如何設(shè)置request header

在發(fā)送Ajax請(qǐng)求(實(shí)質(zhì)是一個(gè)HTTP請(qǐng)求)時(shí),我們可能需要設(shè)置一些請(qǐng)求頭部信息,比如content-type、connection、cookie、accept-xxx等。xhr提供了setRequestHeader來(lái)允許我們修改請(qǐng)求 header。

oid setRequestHeader(DOMString header, DOMString value);

注意點(diǎn)

方法的第一個(gè)參數(shù) header 大小寫不敏感,即可以寫成content-type,也可以寫成Content-Type,甚至寫成content-Type;

Content-Type的默認(rèn)值與具體發(fā)送的數(shù)據(jù)類型有關(guān),請(qǐng)參考本文【可以發(fā)送什么類型的數(shù)據(jù)】一節(jié);

setRequestHeader必須在open()方法之后,send()方法之前調(diào)用,否則會(huì)拋錯(cuò);

setRequestHeader可以調(diào)用多次,最終的值不會(huì)采用覆蓋override的方式,而是采用追加append的方式。下面是一個(gè)示例代碼:

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個(gè)用來(lái)獲取響應(yīng)頭部的方法:getAllResponseHeaders和getResponseHeader。前者是獲取 response 中的所有header 字段,后者只是獲取某個(gè)指定 header 字段的值。另外,getResponseHeader(header)的header參數(shù)不區(qū)分大小寫。

DOMString getAllResponseHeaders();

DOMString getResponseHeader(DOMString header);

這2個(gè)方法看起來(lái)簡(jiǎn)單,但卻處處是坑兒。

你是否遇到過(guò)下面的坑兒?——反正我是遇到了。。。

使用getAllResponseHeaders()看到的所有response header與實(shí)際在控制臺(tái)Network中看到的response header不一樣

使用getResponseHeader()獲取某個(gè)header的值時(shí),瀏覽器拋錯(cuò)Refused to get unsafe header "XXX"

經(jīng)過(guò)一番尋找最終在Stack Overflow找到了答案

原因1:W3C的 xhr 標(biāo)準(zhǔn)中做了限制,規(guī)定客戶端無(wú)法獲取 response 中的Set-Cookie、Set-Cookie2這2個(gè)字段,無(wú)論是同域還是跨域請(qǐng)求;

原因2:W3C 的 cors 標(biāo)準(zhǔn)對(duì)于跨域請(qǐng)求也做了限制,規(guī)定對(duì)于跨域請(qǐng)求,客戶端允許獲取的response header字段只限于“simple response header”和“Access-Control-Expose-Headers” (兩個(gè)名詞的解釋見下方)。

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

"Access-Control-Expose-Headers":首先得注意是"Access-Control-Expose-Headers"進(jìn)行跨域請(qǐng)求時(shí)響應(yīng)頭部中的一個(gè)字段,對(duì)于同域請(qǐng)求,響應(yīng)頭部是沒(méi)有這個(gè)字段的。這個(gè)字段中列舉的 header 字段就是服務(wù)器允許暴露給客戶端訪問(wèn)的字段。

所以getAllResponseHeaders()只能拿到限制以外(即被視為safe)的header字段,而不是全部字段;而調(diào)用getResponseHeader(header)方法時(shí),header參數(shù)必須是限制以外的header字段,否則調(diào)用就會(huì)報(bào)Refused to get unsafe header的錯(cuò)誤。

如何指定xhr.response的數(shù)據(jù)類型

有些時(shí)候我們希望xhr.response返回的就是我們想要的數(shù)據(jù)類型。比如:響應(yīng)返回的數(shù)據(jù)是純JSON字符串,但我們期望最終通過(guò)xhr.response拿到的直接就是一個(gè) js 對(duì)象,我們?cè)撛趺磳?shí)現(xiàn)呢?

有2種方法可以實(shí)現(xiàn),一個(gè)是level 1就提供的overrideMimeType()方法,另一個(gè)是level 2才提供的xhr.responseType屬性。

xhr.overrideMimeType()

overrideMimeType是xhr level 1就有的方法,所以瀏覽器兼容性良好。這個(gè)方法的作用就是用來(lái)重寫response的content-type,這樣做有什么意義呢?比如:server 端給客戶端返回了一份document或者是xml文檔,我們希望最終通過(guò)xhr.response拿到的就是一個(gè)DOM對(duì)象,那么就可以用xhr.overrideMimeType('text/xml; charset = utf-8')來(lái)實(shí)現(xiàn)。

再舉一個(gè)使用場(chǎng)景,我們都知道xhr level 1不支持直接傳輸blob二進(jìn)制數(shù)據(jù),那如果真要傳輸 blob 該怎么辦呢?當(dāng)時(shí)就是利用overrideMimeType方法來(lái)解決這個(gè)問(wèn)題的。

下面是一個(gè)獲取圖片文件的代碼示例:

var xhr = new XMLHttpRequest();

//向 server 端獲取一張圖片

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

// 這行是關(guān)鍵!

//將響應(yīng)數(shù)據(jù)按照純文本格式來(lái)解析,字符集替換為用戶自己定義的字符集

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

xhr.onreadystatechange = function(e) {

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

//通過(guò) responseText 來(lái)獲取圖片文件對(duì)應(yīng)的二進(jìn)制字符串

var binStr = this.responseText;

//然后自己再想方法將逐個(gè)字節(jié)還原為二進(jìn)制數(shù)據(jù)

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請(qǐng)求的是一張圖片,通過(guò)將response的content-type改為'text/plain; charset=x-user-defined',使得xhr以純文本格式來(lái)解析接收到的blob 數(shù)據(jù),最終用戶通過(guò)this.responseText拿到的就是圖片文件對(duì)應(yīng)的二進(jìn)制字符串,最后再將其轉(zhuǎn)換為 blob 數(shù)據(jù)。

xhr.responseType

responseType是xhr level 2新增的屬性,用來(lái)指定xhr.response的數(shù)據(jù)類型,目前還存在些兼容性問(wèn)題,可以參考本文的【XMLHttpRequest的兼容性】這一小節(jié)。那么responseType可以設(shè)置為哪些格式呢,我簡(jiǎn)單做了一個(gè)表,如下:

值 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??xhr.response數(shù)據(jù)類型 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??說(shuō)明

"" ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??String字符串 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?默認(rèn)值(在不設(shè)置responseType時(shí))

"text" ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?String字符串

"document" ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?Document對(duì)象 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??希望返回XML格式數(shù)據(jù)時(shí)使用

"json" ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?javascript對(duì)象 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?存在兼容性問(wèn)題,IE10/IE11不支持

"blob" ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?Blob對(duì)象

"arrayBuffer" ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??ArrayBuffer對(duì)象

下面是同樣是獲取一張圖片的代碼示例,相比xhr.overrideMimeType,用xhr.response來(lái)實(shí)現(xiàn)簡(jiǎn)單得多。

var xhr = new XMLHttpRequest();

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

//可以將`xhr.responseType`設(shè)置為`"blob"`也可以設(shè)置為`" arrayBuffer"`

//xhr.responseType = 'arrayBuffer';

xhr.responseType = 'blob';

xhr.onload = function(e) {

if (this.status == 200) {

var blob = this.response;

...

}

};

xhr.send();

小結(jié)

雖然在xhr level 2中,2者是共同存在的。但其實(shí)不難發(fā)現(xiàn),xhr.responseType就是用來(lái)取代xhr.overrideMimeType()的,xhr.responseType功能強(qiáng)大的多,xhr.overrideMimeType()能做到的xhr.responseType都能做到。所以我們現(xiàn)在完全可以摒棄使用xhr.overrideMimeType()了。

如何獲取response數(shù)據(jù)

xhr提供了3個(gè)屬性來(lái)獲取請(qǐng)求返回的數(shù)據(jù),分別是:xhr.response、xhr.responseText、xhr.responseXML

xhr.response

默認(rèn)值:空字符串""

當(dāng)請(qǐng)求完成時(shí),此屬性才有正確的值

請(qǐng)求未完成時(shí),此屬性的值可能是""或者null,具體與xhr.responseType有關(guān):當(dāng)responseType為""或"text"時(shí),值為"";responseType為其他值時(shí),值為null

xhr.responseText

默認(rèn)值為空字符串""

只有當(dāng)responseType為"text"、""時(shí),xhr對(duì)象上才有此屬性,此時(shí)才能調(diào)用xhr.responseText,否則拋錯(cuò)

只有當(dāng)請(qǐng)求成功時(shí),才能拿到正確值。以下2種情況下值都為空字符串"":請(qǐng)求未完成、請(qǐng)求失敗

xhr.responseXML

默認(rèn)值為null

只有當(dāng)responseType為"text"、""、"document"時(shí),xhr對(duì)象上才有此屬性,此時(shí)才能調(diào)用xhr.responseXML,否則拋錯(cuò)

只有當(dāng)請(qǐng)求成功且返回?cái)?shù)據(jù)被正確解析時(shí),才能拿到正確值。以下3種情況下值都為null:請(qǐng)求未完成、請(qǐng)求失敗、請(qǐng)求成功但返回?cái)?shù)據(jù)無(wú)法被正確解析時(shí)

如何追蹤ajax請(qǐng)求的當(dāng)前狀態(tài)

在發(fā)一個(gè)ajax請(qǐng)求后,如果想追蹤請(qǐng)求當(dāng)前處于哪種狀態(tài),該怎么做呢?

用xhr.readyState這個(gè)屬性即可追蹤到。這個(gè)屬性是只讀屬性,總共有5種可能值,分別對(duì)應(yīng)xhr不同的不同階段。每次xhr.readyState的值發(fā)生變化時(shí),都會(huì)觸發(fā)xhr.onreadystatechange事件,我們可以在這個(gè)事件中進(jìn)行相關(guān)狀態(tài)判斷。

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;

}

值 ? ? ? ? ??狀態(tài) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?描述

0 ? ? ? ? ? ?UNSENT(初始狀態(tài),未打開) ? ? ? ? ?此時(shí)xhr對(duì)象被成功構(gòu)造,open()方法還未被調(diào)用

1 ? ? ? ? ? ?OPENED(已打開,未發(fā)送) ? ? ? ? ? ??open()方法已被成功調(diào)用,send()方法還未被調(diào)用。注意:只有xhr處于OPENED狀態(tài),才能調(diào)用xhr.setRequestHeader()和xhr.send(),否則會(huì)報(bào)錯(cuò)

2 ? ? ? ? ? ?HEADERS_RECEIVED(已獲取響應(yīng)頭) ? ?send()方法已經(jīng)被調(diào)用, 響應(yīng)頭和響應(yīng)狀態(tài)已經(jīng)返回

3 ? ? ? ? ? ?LOADING(正在下載響應(yīng)體) ? ? ? ? ? ??響應(yīng)體(response entity body)正在下載中,此狀態(tài)下通過(guò)xhr.response可能已經(jīng)有了響應(yīng)數(shù)據(jù)

4 ? ? ? ? ? ?DONE(整個(gè)數(shù)據(jù)傳輸過(guò)程結(jié)束) ? ? ? ?整個(gè)數(shù)據(jù)傳輸過(guò)程結(jié)束,不管本次請(qǐng)求是成功還是失敗

如何設(shè)置請(qǐng)求的超時(shí)時(shí)間

如果請(qǐng)求過(guò)了很久還沒(méi)有成功,為了不會(huì)白白占用的網(wǎng)絡(luò)資源,我們一般會(huì)主動(dòng)終止請(qǐng)求。XMLHttpRequest提供了timeout屬性來(lái)允許設(shè)置請(qǐng)求的超時(shí)時(shí)間。

xhr.timeout

單位:milliseconds 毫秒

默認(rèn)值:0,即不設(shè)置超時(shí)

很多同學(xué)都知道:從請(qǐng)求開始算起,若超過(guò)timeout時(shí)間請(qǐng)求還沒(méi)有結(jié)束(包括成功/失敗),則會(huì)觸發(fā)ontimeout事件,主動(dòng)結(jié)束該請(qǐng)求。

【那么到底什么時(shí)候才算是請(qǐng)求開始?】

——xhr.onloadstart事件觸發(fā)的時(shí)候,也就是你調(diào)用xhr.send()方法的時(shí)候。

因?yàn)閤hr.open()只是創(chuàng)建了一個(gè)連接,但并沒(méi)有真正開始數(shù)據(jù)的傳輸,而xhr.send()才是真正開始了數(shù)據(jù)的傳輸過(guò)程。只有調(diào)用了xhr.send(),才會(huì)觸發(fā)xhr.onloadstart。

【那么什么時(shí)候才算是請(qǐng)求結(jié)束?】

——xhr.loadend事件觸發(fā)的時(shí)候。

另外,還有2個(gè)需要注意的坑兒:

1.可以在send()之后再設(shè)置此xhr.timeout,但計(jì)時(shí)起始點(diǎn)仍為調(diào)用xhr.send()方法的時(shí)刻。

2.當(dāng)xhr為一個(gè)sync同步請(qǐng)求時(shí),xhr.timeout必須置為0,否則會(huì)拋錯(cuò)。原因可以參考本文的【如何發(fā)一個(gè)同步請(qǐng)求】一節(jié)。

如何發(fā)一個(gè)同步請(qǐng)求

xhr默認(rèn)發(fā)的是異步請(qǐng)求,但也支持發(fā)同步請(qǐng)求(當(dāng)然實(shí)際開發(fā)中應(yīng)該盡量避免使用)。到底是異步還是同步請(qǐng)求,由xhr.open()傳入的async參數(shù)決定。

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

method: 請(qǐng)求的方式,如GET/POST/HEADER等,這個(gè)參數(shù)不區(qū)分大小寫

url: 請(qǐng)求的地址,可以是相對(duì)地址如example.php,這個(gè)相對(duì)是相對(duì)于當(dāng)前網(wǎng)頁(yè)的url路徑;也可以是絕對(duì)地址如http://www.example.com/example.php

async: 默認(rèn)值為true,即為異步請(qǐng)求,若async=false,則為同步請(qǐng)求

在我認(rèn)真研讀W3C 的 xhr 標(biāo)準(zhǔn)前,我總以為同步請(qǐng)求和異步請(qǐng)求只是阻塞和非阻塞的區(qū)別,其他什么事件觸發(fā)、參數(shù)設(shè)置應(yīng)該是一樣的,事實(shí)證明我錯(cuò)了。

W3C 的 xhr標(biāo)準(zhǔn)中關(guān)于open()方法有這樣一段說(shuō)明:

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.

從上面一段說(shuō)明可以知道,當(dāng)xhr為同步請(qǐng)求時(shí),有如下限制:

xhr.timeout必須為0

xhr.withCredentials必須為false

hr.responseType必須為""(注意置為"text"也不允許)

若上面任何一個(gè)限制不滿足,都會(huì)拋錯(cuò),而對(duì)于異步請(qǐng)求,則沒(méi)有這些參數(shù)設(shè)置上的限制。

之前說(shuō)過(guò)頁(yè)面中應(yīng)該盡量避免使用sync同步請(qǐng)求,為什么呢?

因?yàn)槲覀儫o(wú)法設(shè)置請(qǐng)求超時(shí)時(shí)間(xhr.timeout為0,即不限時(shí))。在不限制超時(shí)的情況下,有可能同步請(qǐng)求一直處于pending狀態(tài),服務(wù)端遲遲不返回響應(yīng),這樣整個(gè)頁(yè)面就會(huì)一直阻塞,無(wú)法響應(yīng)用戶的其他交互。

另外,標(biāo)準(zhǔn)中并沒(méi)有提及同步請(qǐng)求時(shí)事件觸發(fā)的限制,但實(shí)際開發(fā)中我確實(shí)遇到過(guò)部分應(yīng)該觸發(fā)的事件并沒(méi)有觸發(fā)的現(xiàn)象。如在 chrome中,當(dāng)xhr為同步請(qǐng)求時(shí),在xhr.readyState由2變成3時(shí),并不會(huì)觸發(fā)onreadystatechange事件,xhr.upload.onprogress和xhr.onprogress事件也不會(huì)觸發(fā)。

如何獲取上傳、下載的進(jìn)度

在上傳或者下載比較大的文件時(shí),實(shí)時(shí)顯示當(dāng)前的上傳、下載進(jìn)度是很普遍的產(chǎn)品需求。

我們可以通過(guò)onprogress事件來(lái)實(shí)時(shí)顯示進(jìn)度,默認(rèn)情況下這個(gè)事件每50ms觸發(fā)一次。需要注意的是,上傳過(guò)程和下載過(guò)程觸發(fā)的是不同對(duì)象的onprogress事件:

上傳觸發(fā)的是xhr.upload對(duì)象的onprogress事件

下載觸發(fā)的是xhr對(duì)象的onprogress事件

xhr.onprogress = updateProgress;

xhr.upload.onprogress = updateProgress;

function updateProgress(event) {

if (event.lengthComputable) {

var completedPercent = event.loaded / event.total;

}

}

可以發(fā)送什么類型的數(shù)據(jù)

void send(data);

xhr.send(data)的參數(shù)data可以是以下幾種類型:

ArrayBuffer

Blob

Document

DOMString

FormData

null

如果是 GET/HEAD請(qǐng)求,send()方法一般不傳參或傳null。不過(guò)即使你真?zhèn)魅肓藚?shù),參數(shù)也最終被忽略,xhr.send(data)中的data會(huì)被置為null.

xhr.send(data)中data參數(shù)的數(shù)據(jù)類型會(huì)影響請(qǐng)求頭部content-type的默認(rèn)值:

如果data是Document類型,同時(shí)也是HTML Document類型,則content-type默認(rèn)值為text/html;charset=UTF-8;否則為application/xml;charset=UTF-8;

如果data是DOMString類型,content-type默認(rèn)值為text/plain;charset=UTF-8;

如果data是FormData類型,content-type默認(rèn)值為multipart/form-data; boundary=[xxx]

如果data是其他類型,則不會(huì)設(shè)置content-type的默認(rèn)值

當(dāng)然這些只是content-type的默認(rèn)值,但如果用xhr.setRequestHeader()手動(dòng)設(shè)置了中content-type的值,以上默認(rèn)值就會(huì)被覆蓋。

另外需要注意的是,若在斷網(wǎng)狀態(tài)下調(diào)用xhr.send(data)方法,則會(huì)拋錯(cuò):Uncaught NetworkError: Failed to execute 'send' on 'XMLHttpRequest'。一旦程序拋出錯(cuò)誤,如果不 catch 就無(wú)法繼續(xù)執(zhí)行后面的代碼,所以調(diào)用xhr.send(data)方法時(shí),應(yīng)該用try-catch捕捉錯(cuò)誤。

try{

xhr.send(data)

}catch(e) {

//doSomething...

};

xhr.withCredentials與CORS什么關(guān)系

我們都知道,在發(fā)同域請(qǐng)求時(shí),瀏覽器會(huì)將cookie自動(dòng)加在request header中。但大家是否遇到過(guò)這樣的場(chǎng)景:在發(fā)送跨域請(qǐng)求時(shí),cookie并沒(méi)有自動(dòng)加在request header中。

造成這個(gè)問(wèn)題的原因是:在CORS標(biāo)準(zhǔn)中做了規(guī)定,默認(rèn)情況下,瀏覽器在發(fā)送跨域請(qǐng)求時(shí),不能發(fā)送任何認(rèn)證信息(credentials)如"cookies"和"HTTP authentication schemes"。除非xhr.withCredentials為true(xhr對(duì)象有一個(gè)屬性叫withCredentials,默認(rèn)值為false)。

所以根本原因是cookies也是一種認(rèn)證信息,在跨域請(qǐng)求中,client端必須手動(dòng)設(shè)置xhr.withCredentials=true,且server端也必須允許request能攜帶認(rèn)證信息(即response header中包含Access-Control-Allow-Credentials:true),這樣瀏覽器才會(huì)自動(dòng)將cookie加在request header中。

另外,要特別注意一點(diǎn),一旦跨域request能夠攜帶認(rèn)證信息,server端一定不能將Access-Control-Allow-Origin設(shè)置為*,而必須設(shè)置為請(qǐng)求頁(yè)面的域名。

xhr相關(guān)事件

事件分類

xhr相關(guān)事件有很多,有時(shí)記起來(lái)還挺容易混亂。但當(dāng)我了解了具體代碼實(shí)現(xiàn)后,就容易理清楚了。下面是XMLHttpRequest的部分實(shí)現(xiàn)代碼:

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個(gè)事件:

onloadstart

onprogress

onabort

ontimeout

onerror

onload

onloadend

每一個(gè)XMLHttpRequest里面都有一個(gè)upload屬性,而upload是一個(gè)XMLHttpRequestUpload對(duì)象

XMLHttpRequest和XMLHttpRequestUpload都繼承了同一個(gè)XMLHttpRequestEventTarget接口,所以xhr和xhr.upload都有第一條列舉的7個(gè)事件

onreadystatechange是XMLHttpRequest獨(dú)有的事件

所以這么一看就很清晰了:

xhr一共有8個(gè)相關(guān)事件:7個(gè)XMLHttpRequestEventTarget事件+1個(gè)獨(dú)有的onreadystatechange事件;而xhr.upload只有7個(gè)XMLHttpRequestEventTarget事件。

事件觸發(fā)條件

下面是我自己整理的一張xhr相關(guān)事件觸發(fā)條件表,其中最需要注意的是onerror事件的觸發(fā)條件。

事件 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??觸發(fā)條件

onreadystatechange ? ? ? ? ?每當(dāng)xhr.readyState改變時(shí)觸發(fā);但xhr.readyState由非0值變?yōu)?時(shí)不觸發(fā)。

onloadstart ? ? ? ? ? ? ? ? ? ? ? ??調(diào)用xhr.send()方法后立即觸發(fā),若xhr.send()未被調(diào)用則不會(huì)觸發(fā)此事件。

onprogress ? ? ? ? ? ? ? ? ? ? ? ??xhr.upload.onprogress在上傳階段(即xhr.send()之后,xhr.readystate=2之前)觸發(fā),每50ms觸發(fā)一次;xhr.onprogress在下載階段(即xhr.readystate=3時(shí))觸發(fā),每50ms觸發(fā)一次。

onload ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?當(dāng)請(qǐng)求成功完成時(shí)觸發(fā),此時(shí)xhr.readystate=4

onloadend ? ? ? ? ? ? ? ? ? ? ? ? ?當(dāng)請(qǐng)求結(jié)束(包括請(qǐng)求成功和請(qǐng)求失敗)時(shí)觸發(fā)

onabort ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??當(dāng)調(diào)用xhr.abort()后觸發(fā)

ontimeout ? ? ? ? ? ? ? ? ? ? ? ? ? ?xhr.timeout不等于0,由請(qǐng)求開始即onloadstart開始算起,當(dāng)?shù)竭_(dá)xhr.timeout所設(shè)置時(shí)間請(qǐng)求還未結(jié)束即onloadend,則觸發(fā)此事件。

onerror ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?在請(qǐng)求過(guò)程中,若發(fā)生Network error則會(huì)觸發(fā)此事件(若發(fā)生Network error時(shí),上傳還沒(méi)有結(jié)束,則會(huì)先觸發(fā)xhr.upload.onerror,再觸發(fā)xhr.onerror;若發(fā)生Network error時(shí),上傳已經(jīng)結(jié)束,則只會(huì)觸發(fā)xhr.onerror)。注意,只有發(fā)生了網(wǎng)絡(luò)層級(jí)別的異常才會(huì)觸發(fā)此事件,對(duì)于應(yīng)用層級(jí)別的異常,如響應(yīng)返回的xhr.statusCode是4xx時(shí),并不屬于Network error,所以不會(huì)觸發(fā)onerror事件,而是會(huì)觸發(fā)onload事件。

事件觸發(fā)順序

當(dāng)請(qǐng)求一切正常時(shí),相關(guān)的事件觸發(fā)順序如下:

1.觸發(fā)xhr.onreadystatechange(之后每次readyState變化時(shí),都會(huì)觸發(fā)一次)

2.觸發(fā)xhr.onloadstart

//上傳階段開始:

3.觸發(fā)xhr.upload.onloadstart

4.觸發(fā)xhr.upload.onprogress

5.觸發(fā)xhr.upload.onload

6.觸發(fā)xhr.upload.onloadend

//上傳結(jié)束,下載階段開始:

7.觸發(fā)xhr.onprogress

8.觸發(fā)xhr.onload

9.觸發(fā)xhr.onloadend

發(fā)生abort/timeout/error異常的處理

在請(qǐng)求的過(guò)程中,有可能發(fā)生abort/timeout/error這3種異常。那么一旦發(fā)生這些異常,xhr后續(xù)會(huì)進(jìn)行哪些處理呢?后續(xù)處理如下:

1.一旦發(fā)生abort或timeout或error異常,先立即中止當(dāng)前請(qǐng)求

2.將readystate置為4,并觸發(fā)xhr.onreadystatechange事件

3.如果上傳階段還沒(méi)有結(jié)束,則依次觸發(fā)以下事件:

? ??xhr.upload.onprogress

????xhr.upload.[onabort或ontimeout或onerror]

? ??xhr.upload.onloadend

4.觸發(fā)xhr.onprogress事件

5.觸發(fā)xhr.[onabort或ontimeout或onerror]事件

6.觸發(fā)xhr.onloadend事件

在哪個(gè)xhr事件中注冊(cè)成功回調(diào)?

從上面介紹的事件中,可以知道若xhr請(qǐng)求成功,就會(huì)觸發(fā)xhr.onreadystatechange和xhr.onload兩個(gè)事件。 那么我們到底要將成功回調(diào)注冊(cè)在哪個(gè)事件中呢?我傾向于xhr.onload事件,因?yàn)閤hr.onreadystatechange是每次xhr.readyState變化時(shí)都會(huì)觸發(fā),而不是xhr.readyState=4時(shí)才觸發(fā)。

xhr.onload = function () {

//如果請(qǐng)求成功

if(xhr.status == 200){

//do successCallback

}

}

上面的示例代碼是很常見的寫法:先判斷http狀態(tài)碼是否是200,如果是,則認(rèn)為請(qǐng)求是成功的,接著執(zhí)行成功回調(diào)。這樣的判斷是有坑兒的,比如當(dāng)返回的http狀態(tài)碼不是200,而是201時(shí),請(qǐng)求雖然也是成功的,但并沒(méi)有執(zhí)行成功回調(diào)邏輯。所以更靠譜的判斷方法應(yīng)該是:當(dāng)http狀態(tài)碼為2xx或304時(shí)才認(rèn)為成功。

xhr.onload = function () {

//如果請(qǐng)求成功

if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){

//do successCallback

}

}

結(jié)語(yǔ)

最后給點(diǎn)擴(kuò)展學(xué)習(xí)資料,如果你:

想真正搞懂XMLHttpRequest,最靠譜的方法還是看W3C的xhr 標(biāo)準(zhǔn);

想結(jié)合代碼學(xué)習(xí)如何用XMLHttpRequest發(fā)各種類型的數(shù)據(jù),可以參考html5rocks上的這篇文章

想粗略的了解XMLHttpRequest的基本使用,可以參考MDN的XMLHttpRequest介紹

想了解XMLHttpRequest的發(fā)展歷程,可以參考阮老師的文章

想了解Ajax的基本介紹,可以參考AJAX Tutorial;

想了解跨域請(qǐng)求,則可以參考W3C的 cors 標(biāo)準(zhǔn);

想了解http協(xié)議,則可以參考HTTP Tutorial;

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,415評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,647評(píng)論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,130評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,366評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,887評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,737評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,939評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,174評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評(píng)論 1 283
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,608評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,914評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容