有了要連接的 URL 后就可以配置請求了??梢杂?XMLHttpRequest 對象的 open() 方法來(lái)完成。該方法有五個(gè)參數:
?。畆equest-type:發(fā)送請求的類(lèi)型。典型的值是 GET 或 POST,但也可以發(fā)送 HEAD 請求。
?。畊rl:要連接的 URL。
?。產(chǎn)synch:如果希望使用異步連接則為 true,否則為 false。該參數是可選的,默認為 true。
?。畊sername:如果需要身份驗證,則可以在此指定用戶(hù)名。該可選參數沒(méi)有默認值。
?。畃assword:如果需要身份驗證,則可以在此指定口令。該可選參數沒(méi)有默認值。
通常使用其中的前三個(gè)參數。事實(shí)上,即使需要異步連接,也應該指定第三個(gè)參數為 “true”。這是默認值,但堅持明確指定請求是異步的還是同步的更容易理解。
將這些結合起來(lái),通常會(huì )得到 清單 9 所示的一行代碼。
清單 9. 打開(kāi)請求
function getCustomerInfo() {
var phone = document.getElementById("phone").value;
var url = "/cgi-local/lookupCustomer.php?phone=" + escape(phone);
request.open("GET", url, true);
}
一旦設置好了 URL,其他就簡(jiǎn)單了。多數請求使用 GET 就夠了(后面的文章中將看到需要使用 POST 的情況),再加上 URL,這就是使用 open() 方法需要的全部?jì)热萘恕?/p>
挑戰異步性
本系列的后面一篇文章中,我將用很多時(shí)間編寫(xiě)和使用異步代碼,但是您應該明白為什么 open() 的最后一個(gè)參數這么重要。在一般的請求/響應模型中,比如 Web 1.0,客戶(hù)機(瀏覽器或者本地機器上運行的代碼)向服務(wù)器發(fā)出請求。該請求是同步的,換句話(huà)說(shuō),客戶(hù)機等待服務(wù)器的響應。當客戶(hù)機等待的時(shí)候,至少會(huì )用某種形式通知您在等待:
?。陈?特別是 Windows 上)。
?。D的皮球(通常在 Mac 機器上)。
?。畱贸绦蚧旧蟽鼋Y了,然后過(guò)一段時(shí)間光標變化了。
這正是 Web 應用程序讓人感到笨拙或緩慢的原因 —— 缺乏真正的交互性。按下按鈕時(shí),應用程序實(shí)際上變得不能使用,直到剛剛觸發(fā)的請求得到響應。如果請求需要大量服務(wù)器處理,那么等待的時(shí)間可能很長(cháng)(至少在這個(gè)多處理器、DSL 沒(méi)有等待的世界中是如此)。
而異步請求不 等待服務(wù)器響應。發(fā)送請求后應用程序繼續運行。用戶(hù)仍然可以在 Web 表單中輸入數據,甚至離開(kāi)表單。沒(méi)有旋轉的皮球或者沙漏,應用程序也沒(méi)有明顯的凍結。服務(wù)器悄悄地響應請求,完成后告訴原來(lái)的請求者工作已經(jīng)結束(具體的辦法很快就會(huì )看到)。結果是,應用程序感覺(jué)不 那么遲鈍或者緩慢,而是響應迅速、交互性強,感覺(jué)快多了。這僅僅是 Web 2.0 的一部分,但它是很重要的一部分。所有老套的 GUI 組件和 Web 設計范型都不能克服緩慢、同步的請求/響應模型。
發(fā)送請求
一旦用 open() 配置好之后,就可以發(fā)送請求了。幸運的是,發(fā)送請求的方法的名稱(chēng)要比 open() 適當,它就是 send()。
send() 只有一個(gè)參數,就是要發(fā)送的內容。但是在考慮這個(gè)方法之前,回想一下前面已經(jīng)通過(guò) URL 本身發(fā)送過(guò)數據了:
var url = "/cgi-local/lookupCustomer.php?phone=" + escape(phone);
雖然可以使用 send() 發(fā)送數據,但也能通過(guò) URL 本身發(fā)送數據。事實(shí)上,GET 請求(在典型的 Ajax 應用中大約占 80%)中,用 URL 發(fā)送數據要容易得多。如果需要發(fā)送安全信息或 XML,可能要考慮使用 send() 發(fā)送內容(本系列的后續文章中將討論安全數據和 XML 消息)。如果不需要通過(guò) send() 傳遞數據,則只要傳遞 null 作為該方法的參數即可。因此您會(huì )發(fā)現在本文中的例子中只需要這樣發(fā)送請求(參見(jiàn) 清單 10)。
清單 10. 發(fā)送請求
function getCustomerInfo() {
var phone = document.getElementById("phone").value;
var url = "/cgi-local/lookupCustomer.php?phone=" + escape(phone);
request.open("GET", url, true);
request.send(null);
}
指定回調方法
現在我們所做的只有很少一點(diǎn)是新的、革命性的或異步的。必須承認,open() 方法中 “true” 這個(gè)小小的關(guān)鍵字建立了異步請求。但是除此之外,這些代碼與用 Java servlet 及 JSP、PHP 或 Perl 編程沒(méi)有什么兩樣。那么 Ajax 和 Web 2.0 最大的秘密是什么呢?秘密就在于 XMLHttpRequest 的一個(gè)簡(jiǎn)單屬性 onreadystatechange。
首先一定要理解這些代碼中的流程(如果需要請回顧 清單 10)。建立其請求然后發(fā)出請求。此外,因為是異步請求,所以 JavaScript 方法(例子中的 getCustomerInfo())不會(huì )等待服務(wù)器。因此代碼將繼續執行,就是說(shuō),將退出該方法而把控制返回給表單。用戶(hù)可以繼續輸入信息,應用程序不會(huì )等待服務(wù)器。
這就提出了一個(gè)有趣的問(wèn)題:服務(wù)器完成了請求之后會(huì )發(fā)生什么?答案是什么也不發(fā)生,至少對現在的代碼而言如此!顯然這樣不行,因此服務(wù)器在完成通過(guò) XMLHttpRequest 發(fā)送給它的請求處理之后需要某種指示說(shuō)明怎么做。
現在 onreadystatechange 屬性該登場(chǎng)了。該屬性允許指定一個(gè)回調函數?;卣{允許服務(wù)器(猜得到嗎?)反向調用 Web 頁(yè)面中的代碼。它也給了服務(wù)器一定程度的控制權,當服務(wù)器完成請求之后,會(huì )查看 XMLHttpRequest 對象,特別是 onreadystatechange 屬性。然后調用該屬性指定的任何方法。之所以稱(chēng)為回調是因為服務(wù)器向網(wǎng)頁(yè)發(fā)起調用,無(wú)論網(wǎng)頁(yè)本身在做什么。比方說(shuō),可能在用戶(hù)坐在椅子上手沒(méi)有碰鍵盤(pán)的時(shí)候調用該方法,但是也可能在用戶(hù)輸入、移動(dòng)鼠標、滾動(dòng)屏幕或者點(diǎn)擊按鈕時(shí)調用該方法。它并不關(guān)心用戶(hù)在做什么。
這就是稱(chēng)之為異步的原因:用戶(hù)在一層上操作表單,而在另一層上服務(wù)器響應請求并觸發(fā) onreadystatechange 屬性指定的回調方法。因此需要像 清單 11 一樣在代碼中指定該方法。
清單 11. 設置回調方法
function getCustomerInfo() {
var phone = document.getElementById("phone").value;
var url = "/cgi-local/lookupCustomer.php?phone=" + escape(phone);
request.open("GET", url, true);
request.onreadystatechange = updatePage;
request.send(null);
}
需要特別注意的是該屬性在代碼中設置的位置 —— 它是在調用 send()之前設置的。發(fā)送請求之前必須設置該屬性,這樣服務(wù)器在回答完成請求之后才能查看該屬性?,F在剩下的就只有編寫(xiě) updatePage() 方法了,這是本文最后一節要討論的重點(diǎn)。
處理服務(wù)器響應
發(fā)送請求,用戶(hù)高興地使用 Web 表單(同時(shí)服務(wù)器在處理請求),而現在服務(wù)器完成了請求處理。服務(wù)器查看 onreadystatechange 屬性確定要調用的方法。除此以外,可以將您的應用程序看作其他應用程序一樣,無(wú)論是否異步。換句話(huà)說(shuō),不一定要采取特殊的動(dòng)作編寫(xiě)響應服務(wù)器的方法,只需要改變表單,讓用戶(hù)訪(fǎng)問(wèn)另一個(gè) URL 或者做響應服務(wù)器需要的任何事情。這一節我們重點(diǎn)討論對服務(wù)器的響應和一種典型的動(dòng)作 —— 即時(shí)改變用戶(hù)看到的表單中的一部分。
回調和 Ajax
現在我們已經(jīng)看到如何告訴服務(wù)器完成后應該做什么:將 XMLHttpRequest 對象的 onreadystatechange 屬性設置為要運行的函數名。這樣,當服務(wù)器處理完請求后就會(huì )自動(dòng)調用該函數。也不需要擔心該函數的任何參數。我們從一個(gè)簡(jiǎn)單的方法開(kāi)始,如 清單 12 所示。
清單 12. 回調方法的代碼
<script language="javascript" type="text/javascript">
var request = false;
try {
request = new XMLHttpRequest();
} catch (trymicrosoft) {
try {
request = new ActiveXObject("Msxml2.XMLHTTP");
} catch (othermicrosoft) {
try {
request = new ActiveXObject("Microsoft.XMLHTTP");
} catch (failed) {
request = false;
}
}
}
if (!request)
alert("Error initializing XMLHttpRequest!");
function getCustomerInfo() {
var phone = document.getElementById("phone").value;
var url = "/cgi-local/lookupCustomer.php?phone=" + escape(phone);
request.open("GET", url, true);
request.onreadystatechange = updatePage;
request.send(null);
}
function updatePage() {
alert("Server is done!");
}
</script>
它僅僅發(fā)出一些簡(jiǎn)單的警告,告訴您服務(wù)器什么時(shí)候完成了任務(wù)。在自己的網(wǎng)頁(yè)中試驗這些代碼,然后在瀏覽器中打開(kāi)(如果希望查看該例中的 XHTML,請參閱 清單 8)。輸入電話(huà)號碼然后離開(kāi)該字段,將看到一個(gè)彈出的警告窗口,但是點(diǎn)擊 OK 又出現了……
根據瀏覽器的不同,在表單停止彈出警告之前會(huì )看到兩次、三次甚至四次警告。這是怎么回事呢?原來(lái)我們還沒(méi)有考慮 HTTP 就緒狀態(tài),這是請求/響應循環(huán)中的一個(gè)重要部分。
HTTP 就緒狀態(tài)
前面提到,服務(wù)器在完成請求之后會(huì )在 XMLHttpRequest 的 onreadystatechange 屬性中查找要調用的方法。這是真的,但還不完整。事實(shí)上,每當 HTTP 就緒狀態(tài)改變時(shí)它都會(huì )調用該方法。這意味著(zhù)什么呢?首先必須理解 HTTP 就緒狀態(tài)。
HTTP 就緒狀態(tài)表示請求的狀態(tài)或情形。它用于確定該請求是否已經(jīng)開(kāi)始、是否得到了響應或者請求/響應模型是否已經(jīng)完成。它還可以幫助確定讀取服務(wù)器提供的響應文本或數據是否安全。在 Ajax 應用程序中需要了解五種就緒狀態(tài):
?。?:請求沒(méi)有發(fā)出(在調用 open() 之前)。
?。?:請求已經(jīng)建立但還沒(méi)有發(fā)出(調用 send() 之前)。
?。?:請求已經(jīng)發(fā)出正在處理之中(這里通??梢詮捻憫玫絻热蓊^部)。
?。?:請求已經(jīng)處理,響應中通常有部分數據可用,但是服務(wù)器還沒(méi)有完成響應。
?。?:響應已完成,可以訪(fǎng)問(wèn)服務(wù)器響應并使用它。
與大多數跨瀏覽器問(wèn)題一樣,這些就緒狀態(tài)的使用也不盡一致。您也許期望任務(wù)就緒狀態(tài)從 0 到 1、2、3 再到 4,但實(shí)際上很少是這種情況。一些瀏覽器從不報告 0 或 1 而直接從 2 開(kāi)始,然后是 3 和 4。其他瀏覽器則報告所有的狀態(tài)。還有一些則多次報告就緒狀態(tài) 1。在上一節中看到,服務(wù)器多次調用 updatePage(),每次調用都會(huì )彈出警告框 —— 可能和預期的不同!
對于 Ajax 編程,需要直接處理的惟一狀態(tài)就是就緒狀態(tài) 4,它表示服務(wù)器響應已經(jīng)完成,可以安全地使用響應數據了?;诖?,回調方法中的第一行應該如 清單 13 所示。
清單 13. 檢查就緒狀態(tài)
function updatePage() {
if (request.readyState == 4)
alert("Server is done!");
}
修改后就可以保證服務(wù)器的處理已經(jīng)完成。嘗試運行新版本的 Ajax 代碼,現在就會(huì )看到與預期的一樣,只顯示一次警告信息了。
HTTP 狀態(tài)碼
雖然 清單 13 中的代碼看起來(lái)似乎不錯,但是還有一個(gè)問(wèn)題 —— 如果服務(wù)器響應請求并完成了處理但是報告了一個(gè)錯誤怎么辦?要知道,服務(wù)器端代碼應該明白它是由 Ajax、JSP、普通 HTML 表單或其他類(lèi)型的代碼調用的,但只能使用傳統的 Web 專(zhuān)用方法報告信息。而在 Web 世界中,HTTP 代碼可以處理請求中可能發(fā)生的各種問(wèn)題。
比方說(shuō),您肯定遇到過(guò)輸入了錯誤的 URL 請求而得到 404 錯誤碼的情形,它表示該頁(yè)面不存在。這僅僅是 HTTP 請求能夠收到的眾多錯誤碼中的一種(完整的狀態(tài)碼列表請參閱 參考資料 中的鏈接)。表示所訪(fǎng)問(wèn)數據受到保護或者禁止訪(fǎng)問(wèn)的 403 和 401 也很常見(jiàn)。無(wú)論哪種情況,這些錯誤碼都是從完成的響應 得到的。換句話(huà)說(shuō),服務(wù)器履行了請求(即 HTTP 就緒狀態(tài)是 4)但是沒(méi)有返回客戶(hù)機預期的數據。
因此除了就緒狀態(tài)外,還需要檢查 HTTP 狀態(tài)。我們期望的狀態(tài)碼是 200,它表示一切順利。如果就緒狀態(tài)是 4 而且狀態(tài)碼是 200,就可以處理服務(wù)器的數據了,而且這些數據應該就是要求的數據(而不是錯誤或者其他有問(wèn)題的信息)。因此還要在回調方法中增加狀態(tài)檢查,如 清單 14 所示。
清單 14. 檢查 HTTP 狀態(tài)碼
function updatePage() {
if (request.readyState == 4)
if (request.status == 200)
alert("Server is done!");
}
為了增加更健壯的錯誤處理并盡量避免過(guò)于復雜,可以增加一兩個(gè)狀態(tài)碼檢查,請看一看 清單 15 中修改后的 updatePage() 版本。
清單 15. 增加一點(diǎn)錯誤檢查
function updatePage() {
if (request.readyState == 4)
if (request.status == 200)
alert("Server is done!");
else if (request.status == 404)
alert("Request URL does not exist");
else
alert("Error: status code is " + request.status);
}
現在將 getCustomerInfo() 中的 URL 改為不存在的 URL 看看會(huì )發(fā)生什么。應該會(huì )看到警告信息說(shuō)明要求的 URL 不存在 —— 好極了!很難處理所有的錯誤條件,但是這一小小的改變能夠涵蓋典型 Web 應用程序中 80% 的問(wèn)題。
讀取響應文本
現在可以確保請求已經(jīng)處理完成(通過(guò)就緒狀態(tài)),服務(wù)器給出了正常的響應(通過(guò)狀態(tài)碼),最后我們可以處理服務(wù)器返回的數據了。返回的數據保存在 XMLHttpRequest 對象的 responseText 屬性中。
關(guān)于 responseText 中的文本內容,比如格式和長(cháng)度,有意保持含糊。這樣服務(wù)器就可以將文本設置成任何內容。比方說(shuō),一種腳本可能返回逗號分隔的值,另一種則使用管道符(即 | 字符)分隔的值,還有一種則返回長(cháng)文本字符串。何去何從由服務(wù)器決定。
在本文使用的例子中,服務(wù)器返回客戶(hù)的上一個(gè)訂單和客戶(hù)地址,中間用管道符分開(kāi)。然后使用訂單和地址設置表單中的元素值,清單 16 給出了更新顯示內容的代碼。
清單 16. 處理服務(wù)器響應
function updatePage() {
if (request.readyState == 4) {
if (request.status == 200) {
var response = request.responseText.split("|");
document.getElementById("order").value = response[0];
document.getElementById("address").innerHTML =
response[1].replace(/\n/g, "");
} else
alert("status is " + request.status);
}
}
首先,得到 responseText 并使用 JavaScript split() 方法從管道符分開(kāi)。得到的數組放到 response 中。數組中的第一個(gè)值 —— 上一個(gè)訂單 —— 用 response[0] 訪(fǎng)問(wèn),被設置為 ID 為 “order” 的字段的值。第二個(gè)值 response[1],即客戶(hù)地址,則需要更多一點(diǎn)處理。因為地址中的行用一般的行分隔符(“\n”字符)分隔,代碼中需要用 XHTML 風(fēng)格的行分隔符
來(lái)代替。替換過(guò)程使用 replace() 函數和正則表達式完成。最后,修改后的文本作為 HTML 表單 div 中的內部 HTML。結果就是表單突然用客戶(hù)信息更新了。
結束本文之前,我還要介紹 XMLHttpRequest 的另一個(gè)重要屬性 responseXML。如果服務(wù)器選擇使用 XML 響應則該屬性包含(也許您已經(jīng)猜到)XML 響應。處理 XML 響應和處理普通文本有很大不同,涉及到解析、文檔對象模型(DOM)和其他一些問(wèn)題。后面的文章中將進(jìn)一步介紹 XML。但是因為 responseXML 通常和 responseText 一起討論,這里有必要提一提。對于很多簡(jiǎn)單的 Ajax 應用程序 responseText 就夠了,但是您很快就會(huì )看到通過(guò) Ajax 應用程序也能很好地處理 XML。
結束語(yǔ)
您可能對 XMLHttpRequest 感到有點(diǎn)厭倦了,我很少看到一整篇文章討論一個(gè)對象,特別是這種簡(jiǎn)單的對象。但是您將在使用 Ajax 編寫(xiě)的每個(gè)頁(yè)面和應用程序中反復使用該對象。坦白地說(shuō),關(guān)于 XMLHttpRequest 還真有一些可說(shuō)的內容。下一期文章中將介紹如何在請求中使用 POST 及 GET,來(lái)設置請求中的內容頭部和從服務(wù)器響應讀取內容頭部,理解如何在請求/響應模型中編碼請求和處理 XML。
再往后我們將介紹常見(jiàn) Ajax 工具箱。這些工具箱實(shí)際上隱藏了本文所述的很多細節,使得 Ajax 編程更容易。您也許會(huì )想,既然有這么多工具箱為何還要對底層的細節編碼。答案是,如果不知道應用程序在做什么,就很難發(fā)現應用程序中的問(wèn)題。
因此不要忽略這些細節或者簡(jiǎn)單地瀏覽一下,如果便捷華麗的工具箱出現了錯誤,您就不必撓頭或者發(fā)送郵件請求支持了。如果了解如何直接使用 XMLHttpRequest,就會(huì )發(fā)現很容易調試和解決最奇怪的問(wèn)題。只有讓其解決您的問(wèn)題,工具箱才是好東西。
聯(lián)系客服