在上一篇文章中我們知道,AJAX是一系列技術的統稱。在本篇中我們將更進一步,詳細解釋如何使用Ajax技術在項目中獲取數據。而為了解釋清楚,我們首先要搞清楚我們是從哪里獲取數據的,其次我們關注的才是獲取數據的具體方式。
一、獲取數據
我們知道AJAX用來在項目中以阻止頁面刷新的方式獲取數據,那么數據從哪里來呢?我們又怎么知道如何獲取這些數據?答案是我們通常使用API與各式各樣的數據庫交互。
“API”是“Application Programming Interface”(即:應用程序接口)的縮寫,你可以想象一些數據是開放的並且在等待被使用,而我們獲取這些數據的方式便是使用API。API通常的形式是一個URL,並提供指定的參數名和參數值用來幫助你定位所要獲取的數據。
還記得我們提過AJAX需要服務器端的相應設置嗎?我們之后會再來談這一點。
二、AJAX技術的核心 - XMLHttpRequest對象
讓我們先把服務器端的設置拋在一邊,聚焦AJAX技術的核心環節:XMLHttpRequest
對象。
XMLHttpRequest
對象是瀏覽器提供的一個API,用來順暢地向服務器發送請求並解析服務器響應,當然整個過程中,瀏覽器頁面不會被刷新。它將是本文接下來的主角,讓我們先站在較高的層次,對該對象有一個全局的概覽:
XMLHttpRequest
只是一個JavaScript對象,確切的說,是一個構造函數。換句話說,它一點也不神秘,它的特殊之處只在於它是由客戶端(即瀏覽器)提供的(而不是JavaScript原生的),除此之外,它有屬性,有方法,需要通過new
關鍵字進行實例化,我們只需掌握它們就好;XMLHttpRequest
對象是不斷被擴展的。隨着XML對象被廣泛的接收,W3C也開始着手制定相應的標准來規范其行為。目前,XMLHttpRequest
有兩個級別:1級提供了XML對象的實現細節,2級進一步發展了XML對象,額外添加了一些方法,屬性和數據類型。但是,並不是所有瀏覽器都實現了XML對象2級的內容(並不意外,對吧?);
讓我們先從剖析XMLHttpRequest
實例的屬性和方法開始,先創建一個XML對象的實例:
const xhr = new XMLHttpRequest()
該實例的屬性,方法有:
方法
.open()
:准備啟動一個AJAX請求;.setRequestHeader()
:設置請求頭部信息;.send()
:發送AJAX請求;.getResponseHeader()
: 獲得響應頭部信息;.getAllResponseHeader()
:獲得一個包含所有頭部信息的長字符串;.abort()
:取消異步請求;
屬性
.responseText
:包含響應主體返回文本;.responseXML
:如果響應的內容類型時text/xml
或application/xml
,該屬性將保存包含着相應數據的XML DOM文檔;.status
:響應的HTTP狀態;.statusText
:HTTP狀態的說明;.readyState
:表示“請求”/“響應”過程的當前活動階段
另外,瀏覽器還為該對象提供了一個onreadystatechange
監聽事件,每當XML實例的readyState
屬性變化時,就會觸發該事件的發生。
至此,關於XMLHttpRequest實例對象的屬性方法就全部羅列完畢了,接下來,我們將更進一步的探究如何使用這些方法,屬性完成發送AJAX請求的流程。
三、准備AJAX請求
要想與服務器交互,我們首先需要回答以下問題:
- 我們是要獲取數據還是存儲數據? --表現為請求方式的不同:
GET
或POST
; - 向哪里發出請求? --即相應API地址;
- 以何種方式等待響應? --有“同步”和“異步”兩種選擇;(網絡傳輸是一個過程,請求和響應不是同時發生的。)
而XMLHttpRequest實例的.open()
方法的作用就是用來回答以上三個問題。.open()
方法接收三個參數:請求方式,請求URL地址和是否為異步請求的布爾值。
下面是一個.open()
方法調用的例子:
// 該段代碼會啟動一個針對“example.php”的GET同步請求。
xhr.open("get", "example.php", false)
相當於開始做飯前,將工具准備齊備,將菜洗好,.open()
方法也同樣出色地完成了發送AJAX請求的准備工作。
現在,讓我們再深入聊聊一些准備工作的細節:
(一)GET請求 與 POST請求
- GET請求
GET請求用於獲取數據,有時候我們需要獲取的數據需要通過“查詢參數”進行定位,在這種情況下,我們會將查詢參數追加到URL的末尾,令服務器解析。
查詢參數是指一個由?
號起始,由&
符號分割的包含相應鍵值對的字符串。用來告知瀏覽器所要查詢的特定資源。
const query = "example.php?name=tom&age=24" // "?name=tom&age=24"即是一個查詢參數
需要注意的是,查詢字符串中每個參數的名和值都必須使用encodeURIComponent()進行編碼(這是因為URL中有些字符會引起歧義,例如“&”)。
- POST請求
POST請求用於向服務器發送應該被保存的數據,因此POST請求天然比GET請求多需要一份需要被保存的數據。那么這些數據應該放在何處呢?畢竟,我們的.open()
方法接收的三個參數都沒有合適的位置。
答案是需要發送的數據會作為.send()
方法的參數最終被發往服務器,該數據可以是任意大小,任意類型。
這里需要注意以下兩點:
.send()
方法的參數是不可為空的,也就是說,對於不需要發送任何數據的GET請求,也需要在調用.send()
方法時,向其傳入null
值;- 目前為止,我們知道了兩種向服務器發送數據的方式:表單提交以及發送POST請求,要注意服務器對待這兩種方式並不一視同仁,這意味着服務器需要有相應的代碼專門處理POST請求發送來的原始數據。
但好在我們可以通過POST請求模擬表單提交,只需要簡單兩步:
- 設置請求頭參數:
Content-Type: application/x-www-form-urlencoded
(表單提交時的內容類型); - 將表單數據序列化為查詢字符串形式,傳入
.send()
方法;
(二)請求URL地址
這里需要注意若使用相對路徑,請求URL是相對於執行代碼的當前頁面。
(三)同步請求與異步請求
人們通常認為AJAX是異步的,實際上並非如此,AJAX是避免頁面在獲取數據后刷新的一種技術,至於等待服務器響應的方式是同步還是異步,需要開發人員結合業務需求進行配置(雖然通常是異步的)。
你可能會好奇,什么時候我們需要使用同步的AJAX?就我個人經驗而言,似乎很難找到相應的場景。Stack Overflow上有一個類似的問題,有興趣的不妨點擊查看。
最后我們再簡單解釋一下“同步”等待響應與“異步”等待響應的區別:“同步”意味着一旦請求發出,任何后續的JavaScript代碼不會再執行,“異步”則是當請求發出后,后續的JavaScript代碼會繼續執行,當請求成功后,會調用相應的回調函數。
四、設置請求頭
每個HTTP請求和響應都會帶有相應的頭部信息,包含一些與數據,收發者網絡環境與狀態等相關信息。XMLHttpRequest對象提供的.setRequestHeader()
方法為開發者提供了一個操作這兩種頭部信息的方法,並允許開發者自定義請求頭的頭部信息。
默認情況下,當發送AJAX請求時,會附帶以下頭部信息:
Accept
:瀏覽器能夠處理的內容類型;Accept-Charset
: 瀏覽器能夠顯示的字符集;Accept-Encoding
:瀏覽器能夠處理的壓縮編碼;Accept-Language
:瀏覽器當前設置的語言;Connection
:瀏覽器與服務器之間連接的類型;Cookie
:當前頁面設置的任何Cookie;Host
:發出請求的頁面所在的域;Referer
:發出請求的頁面URI;User-Agent
:瀏覽器的用戶代理字符串;
注意,部分瀏覽器不允許使用.setRequestHeader()
方法重寫默認請求頭信息,因此自定義請求頭信息是更加安全的方法:
// 自定義請求頭
xhr.setRequestHeader("myHeader", "MyValue")
五、發送請求
到此為止,我們已經完全做好了發送請求的所有准備:利用.open()
方法確定了請求方式,等待響應的方式和請求地址,甚至還通過.setRequestHeader()
自定義了響應頭,接下來就到了最激動人心的時刻:使用.send()
方法,發送AJAX請求!
// 發送AJAX請求!
const xhr = new XMLHttpRequest()
xhr.open("get", "example.php", false)
xhr.setRequestHeader("myHeader", "goodHeader")
xhr.send(null)
呃,簡單的有些令人尷尬不是嗎?換個POST請求試試看:
// 發送AJAX請求!
const xhr = new XMLHttpRequest()
xhr.open("post", "example.php", false)
xhr.setRequestHeader("myHeader", "bestHeader")
xhr.send(some_data)
額..,總覺得還是差點什么?放輕松伙計,因為我們只是發出了請求,還沒有處理響應,我們這就來看看它。
六、處理響應
讓我們直接看看如何處理一個同步的GET請求響應:
const xhr = new XMLHttpRequest()
xhr.open("get", "example.php", false)
xhr.setRequestHeader("myHeader", "goodHeader")
xhr.send(null)
// 由於是同步的AJAX請求,因此只有當服務器響應后才會繼續執行下面的代碼
// 因此xhr.status的值一定不為默認值
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText)
} else {
alert("Request was unsuccessful: " + xhr.status)
}
上面的代碼不難理解,我們通過之前提到的xhr.status
屬性(如果你忘記了,它存儲着響應的HTTP狀態)判斷請求是否成功,如果成功的話,我們將讀取xhr.responseText
屬性中存儲的返回值。但是,當我們的請求為異步時,問題就稍微變得復雜了,由於是異步的請求,在xhr.send(null)
語句被執行后,JavaScript引擎會緊接着執行下面的判斷語句,而這時由於尚未來得及響應,我們注定會得到一個默認的xhr.status值,因此,我們永遠都不可能獲取請求的資源了。
如何解決這個問題?答案是通過為XMLHTTPRequest實例添加onreadystatechange
事件處理程序(當然你也可以直接使用DOM2級規范規定的.addEventListener()
方法,但是注意,IE8是不支持該方法的)。
xhr實例的readystatechange
事件會監聽xhr.readyState
屬性的變化,你可以將這個屬性想象為一個計數器,隨着AJAX流程的推進而不斷累加,其可取的值如下:
- 0:未初始化 -- 尚未調用
.open()
方法; - 1:啟動 -- 已經調用
.open()
方法,但尚未調用.send()
方法; - 2:發送 -- 已經調用
.send()
方法,但尚未接收到響應; - 3:接收 -- 已經接收到部分響應數據;
- 4:完成 -- 已經接收到全部響應數據,而且已經可以在客戶端使用了;
有了這個時間處理程序對AJAX進程做監聽,剩下的事就簡單多了,一個異步的GET請求代碼如下:
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readystate == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText)
} else {
alert("Request was unsuccessful: " + xhr.status)
}
}
}
xhr.open("get", "example.php", true)
xhr.send(null)
注意:為了確保跨瀏覽器的兼容性,必須要在調用.open()
方法之前指定事件處理程序,仔細想想也有道理,畢竟.open()
方法的執行也包含在該事件處理程序的監聽范圍之內對吧?
七、取消異步請求
有時候,你可能需要在接收到響應之前取消異步請求,這時候,你需要調用.abort()
方法。
該方法會令XHR對象實例停止觸發事件,並且不再允許訪問任何和響應有關的對象屬性。沒了監控器,我們再也沒法判斷響應了不是嗎?
但是需要注意的是,當終止AJAX請求后,你需要手動對XHR對象實例進行解綁以釋放內存空間。
🎉🎉 恭喜你!到這里你已經學會了所有的AJAX基礎知識,你知道了AJAX是什么,存在的意義以及如何真正發起一個AJAX請求並接收響應,你已經是一個AJAX大師!祝賀你!太棒了!🎉🎉
🤜 真棒,尊敬的AJAX大師,你居然還沒有離開,那么我將傳授你最后一部分AJAX秘籍,幫助你成為一個真正的AJAX忍者,這是你的堅持贏得的!
八、秘籍:XMLHttpRequest 2級
還記得我們一開始有提到,W3C提出了XMLHttpRequest 2級規范嗎?雖然並非所有的瀏覽器都實現了該規范所規定的內容,但還是有一些內容被全部或大多數瀏覽器所實現。想成為AJAX忍者?往下看吧。
提示:在這一部分,你將會看到很多有關瀏覽器兼容性的文字,希望你不要覺得枯燥,畢竟這可是忍者的修行,對吧?
(一)FormData 類型
FormData是XMLHttpRequest 2級為我們提供的新的數據類型(構造函數),還記的我們之前是如何偽裝一個POST請求為一個表單提交嗎?FormData令這一過程變得更加輕松,因為XHR對象能夠識別傳入的數據類型是FormData的實例,並自動配置適當的頭部信息。
FormData的使用方式如下:
// 添加數據
let data1 = new FormData()
data1.append("name", "Tom")
xhr.send(data1)
// 提取表單數據
let data2 = new FormData(document.forms[0])
xhr.send(data2)
除此之外,FormData的另一個好處是相較於傳統的AJAX請求,它允許我們上傳二進制數據(圖片,視頻,音頻等),具體詳情可查看該鏈接。
FormData的瀏覽器兼容性:
- 桌面端
- IE 10+ 與其他瀏覽器均支持
- 移動端
- Android,Firefox Mobile,OperaMobile均支持,其余瀏覽器未知
(二)超時設定
當我們發送一個AJAX請求,卻遲遲得不到服務器響應,這種感覺是很糟糕的。為了緩解這種糟糕的感覺,XMLHttpRequest 2級規范為我們提供了一個額外的屬性和事件監聽事件:
timeout
屬性:設置超時時間,單位為毫秒;timeout
事件:當響應時間超出實例對象timeout屬性時被觸發;
使用方式如下:
// 當響應時間超過1秒時,請求中止,彈出提示框
xhr.timeout = 1000
xhr.ontimeout = () => { alert("Request did not return in a second.") }
注意,當請求終止時,會調用ontimeout
事件處理程序,此時xhr的readyState
屬性的值可能已變為4,這意味着會繼續調用onreadystatechange
事件處理程序,但是當超時中止請求后再訪問xhr的status
屬性會使瀏覽器拋出一個錯誤,因此需要將檢查status
屬性的語句放入try-catch
語句中。
雖然帶來了一些麻煩,但是我們卻對XMLHttpRequest對象有了更多的控制。
瀏覽器兼容性:
- 桌面端
- IE 10+ 與其他瀏覽器均支持
- 移動端
- IE Mobile 10+ 與其他瀏覽器均支持
(三)overrideMimeType()方法
響應返回的響應頭里,描述了返回數據的MIME類型,瀏覽器通過識別該類型,告知XMLHttpRequest實例處理該數據的方式。然而有時候(例如將XML類型數據當做純文本處理),我們想要以我們想要的方式處理響應的數據,在XMLHttpRequest 2級規范中,我們可以使用.overrideMimeType()
方法,從方法名也可以輕松猜出,該方法可以覆寫響應頭所描述數據的MIME類型。
其寫法如下:
const xhr = new XMLHttpRequest()
xhr.open("get", "example.php", true)
xhr.overrideMimeType("text/xml") // 強迫瀏覽器將響應數據以指定類型方式解讀
xhr.send(null)
至此,我們掌控了響應數據的處理方式。
瀏覽器兼容性:
- 桌面端
- IE 7+ 與其他瀏覽器均支持
- 移動端
- Firefox Mobile,Chrome for Android 均支持,其余瀏覽器未知
(四)進度事件
Progress Events規范是W3C制定的一個工作草案。該規范定義了與客戶端與服務器通信相關的一系列事件,這些事件監聽了通信進程中的各個關鍵節點,使我們能夠以更細的顆粒度掌控數據傳輸過程中的細節。目前共有6個進度事件,他們會隨數據傳輸進展被順序觸發(除了error,abort事件),讓我們看看他們的定義和瀏覽器兼容情況:
loadstart
:在接收到響應數據的第一個字節時觸發;- 桌面端:除 Safari Mobile 未知外,其他瀏覽器均支持
- 移動端:除 Safari Mobile 未知外,其他瀏覽器均支持
progress
:在接收響應期間持續不斷地觸發;- 桌面端:IE10+ 與其他瀏覽器均支持
- 移動端:均支持
error
:在請求發生錯誤時觸發;abort
:再因為調用abort()
方法時觸發;- 桌面端:未知
- 移動端:未知
load
:在接收到完整的響應數據時觸發;- 桌面端:IE7+ 與其他瀏覽器均支持
- 移動端:Chrome for Android,Edge,Firefox Mobile支持,其余瀏覽器未知
loadend
:在通信完成或者觸發error
,abort
或load
事件后觸發;- 桌面端:所有瀏覽器不支持
- 移動端:所有瀏覽器不支持
這里我們將着重展開講解以下兩個事件:
① load事件
該事件幫助我們節省了readstatechange
事件,我們不必在XHR對象實例上綁定該事件監聽函數以追蹤實例上readState
屬性的變化,而是可以直接使用以下代碼:
const xhr = new XMLHttpRequest()
xhr.onload = () => {
if ((xhr.status >= 200 && xhr.status <300) || xhr.status == 304) {
alert(xhr.responseText)
} else {
alert("Something wrong!")
}
}
xhr.open("get", "example.php", true)
xhr.send(null)
② progress事件
該事件令我們可以實現我們夢寐以求的加載進度條效果。因為onprogress
事件處理程序會接收到一個event
對象,其target
屬性為XHR對象實例,但卻額外包含着三個屬性:
lengthComputable
:表示進度信息是否可用的布爾值;position
:表示目前接收的字節數;totalSize
:表示根據Content-Length響應頭部確定的預期字節數;
很顯然,我們的加載進度條所需的一切資源都准備就緒,我們只需寫出下面的代碼:
const xhr = new XMLHttpRequest()
xhr.onload = () => {
if ((xhr.status >= 200 && xhr.status <300) || xhr.status == 304) {
alert(xhr.responseText)
} else {
alert("Something wrong!")
}
}
// 加載進度條
xhr.onprogress = function(event) {
const divStatus = document.getElementById("status")
if (event.lengthComputable) {
divStatus.innerHTML = `Received ${event.postion} of ${event.totalSize} bytes`
}
}
xhr.open("get", "example.php", true)
xhr.send(null)
一切大功告成!不過還要記得注意,需要在.open()
方法前調用onprogress
事件處理程序。
太棒了,關於AJAX,我已經沒有什么可說的了,如果你已經掌握了以上所有概念,那么“AJAX忍者”的稱號你當之無愧。
我真的為你感到驕傲,Great Work!🙌
