AJAX和XMLHttpRequest物件
前言
在web發展的早期,頁面上的互動元素(比如表單的提交按鈕)的工作模式是使用者點選,瀏覽器發起請求,使用者等待,伺服器返回新的頁面,瀏覽器重新整理頁面,如果網速比較差,那麼使用者需要等待很長的事件。雖然web的發展,網頁上的元素越來越多,也不侷限於文字,越來越多的圖片出現在網頁上,原來的這種點選,等待,重新整理的機制讓使用者體驗非常糟糕。順應著這種需求,ajax誕生了。
AJAX
AJAX是一種技術方案,讓瀏覽器能夠異步向伺服器請求資料而不用重新整理頁面,AJAX的核心就是瀏覽器的XMLHttpRequest
物件,這個物件最早由微軟實現,後來各大瀏覽器都提供了對XMLHtmlRequest
物件的支援。其實在XMLHtmlRequest
出現之前,很多開發人員就在嘗試這種模式的通訊了,當時都是使用一些hack的手段。XMLHttpRequest
物件的出現為向伺服器傳送HTTP請求和接收HTTP響應提供了流暢的介面,讓開發人員能夠方便地用非同步的方法於伺服器進行互動,這種非同步的通訊方法在2005年正式被命名為ajax
。正是ajax技術極大的提高了web應用的使用者體驗和效能,是web飛速發展的一個起點。
XHR標準
從微軟在IE5
上引入XHR
物件以後,其他瀏覽器也都增加了對XHR
物件的支援,如今XHR
已經有了通用的標準
,最新版的標準是XHR level 2
,level1
的用法如下:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'example.php'); xhr.send(); xhr.onreadystatechange = function(){ if ( xhr.readyState == 4 && xhr.status == 200 ) { alert( xhr.responseText ); } else { alert( xhr.statusText ); } };
level1
的主要屬性如下:
– xhr.readyState:XMLHttpRequest物件的狀態,等於4表示資料已經接收完畢。
– xhr.status:伺服器返回的狀態碼,等於200表示一切正常。
– xhr.responseText:伺服器返回的文字資料
– xhr.responseXML:伺服器返回的XML格式的資料
– xhr.statusText:伺服器返回的狀態文字。
level1
有以下幾個缺點:
– 受同源策略的限制,不能傳送跨域請求;
– 不能傳送二進位制檔案(如圖片、視訊、音訊等),只能傳送純文字資料;
– 在傳送和獲取資料的過程中,無法實時獲取進度資訊,只能判斷是否完成;
level2
對level1
進行了改進,增加了以下特性:
– 可以傳送跨域請求,在服務端允許的情況下;
– 支援傳送和接收二進位制資料;
– 新增formData物件,支援傳送表單資料;
– 傳送和獲取資料時,可以獲取進度資訊;
– 可以設定請求的超時時間;
XHR的使用
設定請求首部
HTTP請求報文有很多首部資訊,首部中會提供請求的許多資訊,比如優先的字符集,編碼等,XHR
物件提供了讓我們設定請求首部的方法setRequestHeader()
。
XMLHttpRequest.setRequestHeader(header, value)
setRequestHeader()
方法有幾點需要注意:
1. setRequestHeader必須在open()方法之後,send()方法之前呼叫,否則會拋錯;
2. setRequestHeader可以呼叫多次,最終的值不會採用覆蓋override的方式,而是採用追加append的方式。下面是一個示例程式碼:
3. 並不是所有首部欄位都可以設定,參照禁止修改的首部列表
// The following script: var client = new XMLHttpRequest(); client.open('GET', 'demo.cgi'); client.setRequestHeader('X-Test', 'one'); client.setRequestHeader('X-Test', 'two'); client.send(); // …results in the following header being sent: // X-Test: one, two
獲取Response Header
xhr提供了2個用來獲取響應頭部的方法:getAllResponseHeaders()
和getResponseHeader()
。前者是獲取response
中的所有header
欄位,後者只是獲取某個指定header
欄位的值。另外getResponseHeader(header)
的header
引數不區分大小寫。
var headers = XMLHttpRequest.getAllResponseHeaders();
var myHeader = XMLHttpRequest.getResponseHeader(headerName);
這兩個方法對於獲取Response Header
還是有一定的限制:
1. 無法獲取 response 中的 Set-Cookie、Set-Cookie2這2個欄位,無論是同域還是跨域請求;
2. 對於跨域請求,客戶端允許獲取的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 欄位就是伺服器允許暴露給客戶端訪問的欄位。
重寫響應的MIME型別
MIME型別
媒體型別(通常稱為 Multipurpose Internet Mail Extensions 或 MIME 型別 )是一種標準,用來表示文件、檔案或位元組流的性質和格式。它在IETF RFC 6838中進行了定義和標準化。詳見[MDN](媒體型別(通常稱為 Multipurpose Internet Mail Extensions 或 MIME 型別 )是一種標準,用來表示文件、檔案或位元組流的性質和格式。它在IETF RFC 6838中進行了定義和標準化。 “MDN”)
型別 | 描述 | 典型示例 |
---|---|---|
text | 表明檔案是普通文字,理論上是人類可讀 | text/plain, text/html, text/css, text/javascript |
image | 表明是某種影象。不包括視訊,但是動態圖(比如動態gif)也使用image型別 | image/gif, image/png, image/jpeg, image/bmp, image/webp, image/x-icon, image/vnd.microsoft.icon |
audio | 表明是某種音訊檔案 | audio/midi, audio/mpeg, audio/webm, audio/ogg, audio/wav |
video | 表明是某種視訊檔案 | video/webm, video/ogg |
application | 表明是某種二進位制資料 | application/octet-stream, application/pkcs12, application/vnd.mspowerpoint, application/xhtml+xml, application/xml,application/pdf |
瀏覽器通常使用MIME型別(而不是副檔名)來確定如何處理URL,因此Web伺服器在響應頭中新增正確的MIME型別非常重要。如果配置不正確,瀏覽器可能會曲解檔案內容,網站將無法正常工作,並且下載的檔案也會被錯誤處理。
對於伺服器返回的內容,可能由於響應報文中的content-type
和實際內容不符而導致瀏覽器對資料的處理出現問題,有時候需要告訴瀏覽器返回的內容按什麼媒體型別處理。XHR給出的方法有兩個:xhr.overrideMimeType()
方法和xhr.responseType
屬性。
xhr.overrideMimeType()
XMLHttpRequest
的overrideMimeType
方法是指定一個MIME型別用於替代伺服器指定的型別,使服務端響應資訊中傳輸的資料按照該指定MIME型別處理。例如強制使流方式處理為”text/xml
“型別處理時會被使用到,即使伺服器在響應頭中並沒有這樣指定。此方法必須在send
方法之前呼叫方為有效。
如果伺服器沒有指定一個Content-Type
頭,XMLHttpRequest
預設MIME型別為”text/xml
“. 如果接受的資料不是有效的XML
,將會出現格”格式不正確“的錯誤。你能夠通過呼叫overrideMimeType()
指定各種型別來避免這種情況。
req = new XMLHttpRequest(); req.overrideMimeType("text/plain"); req.addEventListener("load", callback, false); req.open("get", url); req.send();
xhr.responseType
XMLHttpRequest.responseType
屬性是一個列舉型別的屬性,返回響應資料的型別。它允許我們手動的設定返回資料的型別。如果我們將它設定為一個空字串,它將使用預設的”text
“型別。
當將responseType
設定為一個特定的型別時,你需要確保伺服器所返回的型別和你所設定的返回值型別是相容的。那麼如果兩者型別不相容,伺服器返回的資料會變成null
,即使伺服器返回了資料。還有一個要注意的是,給一個同步請求設定responseType
會丟擲一個InvalidAccessError
的異常。
responseType
支援以下幾種型別:
值 | 描述 |
---|---|
“” | 將 responseType 設為空字串與設定為”text”相同, 是預設型別 (實際上是 DOMString)。 |
“arraybuffer” | response 是一個包含二進位制資料的 JavaScript ArrayBuffer 。 |
“blob” | response 是一個包含二進位制資料的 Blob 物件 。 |
“document” | response 是一個 HTML Document 或 XML XMLDocument ,這取決於接收到的資料的 MIME |型別。請參閱 HTML in XMLHttpRequest 以瞭解使用 XHR 獲取 HTML 內容的更多資訊。 |
“json” | response 是一個 JavaScript 物件。這個物件是通過將接收到的資料型別視為 JSON 解析得到的。 |
“text” | response 是包含在 DOMString 物件中的文字。 |
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();
獲取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
:請求未完成、請求失敗、請求成功但返回資料無法被正確解析時
-
預設值為
獲取請求狀態
用xhr.readyState
這個屬性可以獲取當前請求的狀態。這個屬性是隻讀屬性,總共有5種可能值,分別對應xhr
不同的不同階段。每次xhr.readyState
的值發生變化時,都會觸發xhr.onreadystatechange
事件,我們可以在這個事件中進行相關狀態判斷。
值 | 狀態 | 描述 |
---|---|---|
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 (整個資料傳輸過程結束) | 整個資料傳輸過程結束,不管本次請求是成功還是失敗 |
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; }
設定請求超時時間
如果請求過了很久還沒有成功,為了不會白白佔用的網路資源,我們一般會主動終止請求。XMLHttpRequest
提供了timeout
屬性來允許設定請求的超時時間。
XMLHttpRequest.timeout
是一個無符號長整型數,代表著一個請求在被自動終止前所消耗的毫秒數。預設值為 0,意味著沒有超時。超時並不應該用在一個 同步XMLHttpRequests
請求中,否則將會丟擲一個InvalidAccessError
型別的錯誤。當超時發生,timeout
事件將會被觸發。
xhr.onloadstart
事件觸發的時候開始計時,也就是你呼叫xhr.send()
方法的時候。
可以在send()
之後再設定此xhr.timeout
,但計時起始點仍為呼叫xhr.send()
方法的時刻。
var xhr = new XMLHttpRequest(); xhr.open('GET', '/server', true); xhr.timeout = 2000; // 超時時間,單位是毫秒 xhr.onload = function () { // 請求完成。在此進行處理。 }; xhr.ontimeout = function (e) { // XMLHttpRequest 超時。在此做某事。 }; xhr.send(null);
同步請求
xhr
預設發的是非同步請求,但也支援發同步請求(當然實際開發中應該儘量避免使用)。到底是非同步還是同步請求,由xhr.open()
傳入的async
引數決定。
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
,則為同步請求.
當xhr為同步請求時,有如下限制:
- xhr.timeout必須為0
- xhr.withCredentials必須為 false
- xhr.responseType必須為””(注意置為”text”也不允許)
在 chrome中,當xhr
為同步請求時,在xhr.readyState
由2變成3時,並不會觸發onreadystatechange
事件,xhr.upload.onprogress
和xhr.onprogress
事件也不會觸發。
獲取上傳、下載的進度
我們可以通過onprogress
事件來實時顯示進度,預設情況下這個事件每50ms
觸發一次。需要注意的是,上傳過程和下載過程觸發的是不同物件的onprogress
事件:
onprogress 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 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
的值,以上預設值就會被覆蓋。
FormData物件
ajax操作往往用來傳遞表單資料。為了方便表單處理,HTML 5新增了一個FormData物件,可以模擬表單。
var formData = new FormData(); formData.append('username', '張三'); formData.append('id', 123456); xhr.send(formData); //FormData物件也可以用來獲取網頁表單的值。 var form = document.getElementById('myform'); var formData = new FormData(form); formData.append('secret', '123456'); // 新增一個表單項 xhr.open('POST', form.action); xhr.send(formData);
上傳檔案
假定files
是一個”選擇檔案”的表單元素(input[type="file"]
),我們將它裝入FormData
,
javascript var formData = new FormData(); for (var i = 0; i < files.length;i++) { formData.append('files[]', files[i]); } xhr.send(formData);物件。
xhr.withCredentials
我們都知道,在發同域請求時,瀏覽器會將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
設定為*
,而必須設定為請求頁面的域名。
XMLHttpRequest
事件
XMLHttpRequestEventTarget
介面定義了7個事件:
onloadstart onprogress onabort ontimeout onerror onload onloadend
-
每一個
XMLHttpRequest
裡面都有一個upload
屬性,而upload
是一個XMLHttpRequestUpload
(表示上傳進度)物件 -
XMLHttpRequest
和XMLHttpRequestUpload
都繼承了同一個XMLHttpRequestEventTarget
介面,所以xhr
和xhr.upload
都有第一條列舉的7個事件 -
onreadystatechange
是XMLHttpRequest
獨有的事件
事件 | 觸發條件 |
---|---|
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
)。注意,只有發生了網路層級別的異常才會觸發此事件,對於應用層級別的異常,如響應返回的xhr.status
是4xx
時,並不屬於Network error
,所以不會觸發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這3種異常。
-
一旦發生
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
事件