得到請求對象之後就可以進入請求/響應循環了。記住,XMLHttpRequest
惟一的目的是讓您發送請求和接收響應。其他一切都是 JavaScript、CSS 或頁面中其他代碼的工作:改變用戶界面、切換圖像、解釋服務器返回的數據。準備好 XMLHttpRequest
之後,就可以向服務器發送請求了。
Ajax 採用一種沙箱安全模型。因此,Ajax 代碼(具體來說就是 XMLHttpRequest
對象)只能對所在的同一個域發送請求。以後的文章中將進一步介紹安全和 Ajax,現在只要知道在本地機器上運行的代碼只能對本地機器上的服務器端腳本發送請求。如果讓 Ajax 代碼在 www.breakneckpizza.com 上運行,則必須 www.breakneck.com 中運行的腳本發送請求。
首先要確定連接的服務器的 URL。這並不是 Ajax 的特殊要求,但仍然是建立連接所必需的,顯然現在您應該知道如何構造 URL 了。多數應用程序中都會結合一些靜態數據和用戶處理的表單中的數據來構造該 URL。比如,清單 7 中的 JavaScript 代碼獲取電話號碼字段的值並用其構造 URL。
<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);
}
</script> |
這裏沒有難懂的地方。首先,代碼創建了一個新變量 phone
,並把 ID 爲 “phone” 的表單字段的值賦給它。清單 8 展示了這個表單的 XHTML,其中可以看到 phone
字段及其 id
屬性。
<body>
<p><img src="breakneck-logo_4c.gif" alt="Break Neck Pizza" /></p>
<form action="POST">
<p>Enter your phone number:
<input type="text" size="14" name="phone" id="phone"
onChange="getCustomerInfo();" />
</p>
<p>Your order will be delivered to:</p>
<div id="address"></div>
<p>Type your order in here:</p>
<p><textarea name="order" rows="6" cols="50" id="order"></textarea></p>
<p><input type="submit" value="Order Pizza" id="submit" /></p>
</form>
</body> |
還要注意,當用戶輸入電話號碼或者改變電話號碼時,將觸發 清單 8 所示的 getCustomerInfo()
方法。該方法取得電話號碼並構造存儲在 url
變量中的 URL 字符串。記住,由於 Ajax 代碼是沙箱型的,因而只能連接到同一個域,實際上 URL 中不需要域名。該例中的腳本名爲 /cgi-local/lookupCustomer.php
。最後,電話號碼作爲 GET 參數附加到該腳本中:"phone=" + escape(phone)
。
如果以前沒用見過 escape()
方法,它用於轉義不能用明文正確發送的任何字符。比如,電話號碼中的空格將被轉換成字符 %20
,從而能夠在 URL 中傳遞這些字符。
可以根據需要添加任意多個參數。比如,如果需要增加另一個參數,只需要將其附加到 URL 中並用 “與”(&
)字符分開 [第一個參數用問號(?
)和腳本名分開]。
|
有了要連接的 URL 後就可以配置請求了。可以用 XMLHttpRequest
對象的 open()
方法來完成。該方法有五個參數:
- request-type:發送請求的類型。典型的值是
GET
或POST
,但也可以發送HEAD
請求。 - url:要連接的 URL。
- asynch:如果希望使用異步連接則爲 true,否則爲 false。該參數是可選的,默認爲 true。
- username:如果需要身份驗證,則可以在此指定用戶名。該可選參數沒有默認值。
- password:如果需要身份驗證,則可以在此指定口令。該可選參數沒有默認值。
通常使用其中的前三個參數。事實上,即使需要異步連接,也應該指定第三個參數爲 “true”。這是默認值,但堅持明確指定請求是異步的還是同步的更容易理解。
將這些結合起來,通常會得到 清單 9 所示的一行代碼。
function getCustomerInfo() {
var phone = document.getElementById("phone").value;
var url = "/cgi-local/lookupCustomer.php?phone=" + escape(phone);
request.open("GET", url, true);
} |
一旦設置好了 URL,其他就簡單了。多數請求使用 GET
就夠了(後面的文章中將看到需要使用 POST
的情況),再加上 URL,這就是使用 open()
方法需要的全部內容了。
本系列的後面一篇文章中,我將用很多時間編寫和使用異步代碼,但是您應該明白爲什麼 open()
的最後一個參數這麼重要。在一般的請求/響應模型中,比如 Web 1.0,客戶機(瀏覽器或者本地機器上運行的代碼)向服務器發出請求。該請求是同步的,換句話說,客戶機等待服務器的響應。當客戶機等待的時候,至少會用某種形式通知您在等待:
- 沙漏(特別是 Windows 上)。
- 旋轉的皮球(通常在 Mac 機器上)。
- 應用程序基本上凍結了,然後過一段時間光標變化了。
這正是 Web 應用程序讓人感到笨拙或緩慢的原因 —— 缺乏真正的交互性。按下按鈕時,應用程序實際上變得不能使用,直到剛剛觸發的請求得到響應。如果請求需要大量服務器處理,那麼等待的時間可能很長(至少在這個多處理器、DSL 沒有等待的世界中是如此)。
而異步請求不 等待服務器響應。發送請求後應用程序繼續運行。用戶仍然可以在 Web 表單中輸入數據,甚至離開表單。沒有旋轉的皮球或者沙漏,應用程序也沒有明顯的凍結。服務器悄悄地響應請求,完成後告訴原來的請求者工作已經結束(具體的辦法很快就會看到)。結果是,應用程序感覺不 那麼遲鈍或者緩慢,而是響應迅速、交互性強,感覺快多了。這僅僅是 Web 2.0 的一部分,但它是很重要的一部分。所有老套的 GUI 組件和 Web 設計範型都不能克服緩慢、同步的請求/響應模型。
一旦用 open()
配置好之後,就可以發送請求了。幸運的是,發送請求的方法的名稱要比 open()
適當,它就是 send()
。
send()
只有一個參數,就是要發送的內容。但是在考慮這個方法之前,回想一下前面已經通過 URL 本身發送過數據了:
var url = "/cgi-local/lookupCustomer.php?phone=" + escape(phone); |
雖然可以使用 send()
發送數據,但也能通過 URL 本身發送數據。事實上,GET
請求(在典型的 Ajax 應用中大約佔 80%)中,用 URL 發送數據要容易得多。如果需要發送安全信息或 XML,可能要考慮使用 send()
發送內容(本系列的後續文章中將討論安全數據和 XML 消息)。如果不需要通過 send()
傳遞數據,則只要傳遞 null
作爲該方法的參數即可。因此您會發現在本文中的例子中只需要這樣發送請求(參見 清單 10)。
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);
} |
現在我們所做的只有很少一點是新的、革命性的或異步的。必須承認,open()
方法中 “true” 這個小小的關鍵字建立了異步請求。但是除此之外,這些代碼與用 Java servlet 及 JSP、PHP 或 Perl 編程沒有什麼兩樣。那麼 Ajax 和 Web 2.0 最大的祕密是什麼呢?祕密就在於 XMLHttpRequest
的一個簡單屬性 onreadystatechange
。
首先一定要理解這些代碼中的流程(如果需要請回顧 清單 10)。建立其請求然後發出請求。此外,因爲是異步請求,所以 JavaScript 方法(例子中的 getCustomerInfo()
)不會等待服務器。因此代碼將繼續執行,就是說,將退出該方法而把控制返回給表單。用戶可以繼續輸入信息,應用程序不會等待服務器。
這就提出了一個有趣的問題:服務器完成了請求之後會發生什麼?答案是什麼也不發生,至少對現在的代碼而言如此!顯然這樣不行,因此服務器在完成通過 XMLHttpRequest
發送給它的請求處理之後需要某種指示說明怎麼做。
|
現在 onreadystatechange
屬性該登場了。該屬性允許指定一個回調函數。回調允許服務器(猜得到嗎?)反向調用 Web 頁面中的代碼。它也給了服務器一定程度的控制權,當服務器完成請求之後,會查看 XMLHttpRequest
對象,特別是 onreadystatechange
屬性。然後調用該屬性指定的任何方法。之所以稱爲回調是因爲服務器向網頁發起調用,無論網頁本身在做什麼。比方說,可能在用戶坐在椅子上手沒有碰鍵盤的時候調用該方法,但是也可能在用戶輸入、移動鼠標、滾動屏幕或者點擊按鈕時調用該方法。它並不關心用戶在做什麼。
這就是稱之爲異步的原因:用戶在一層上操作表單,而在另一層上服務器響應請求並觸發 onreadystatechange
屬性指定的回調方法。因此需要像 清單 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()
之前 設置的。發送請求之前必須設置該屬性,這樣服務器在回答完成請求之後才能查看該屬性。現在剩下的就只有編寫 updatePage()
方法了,這是本文最後一節要討論的重點。
發送請求,用戶高興地使用 Web 表單(同時服務器在處理請求),而現在服務器完成了請求處理。服務器查看 onreadystatechange
屬性確定要調用的方法。除此以外,可以將您的應用程序看作其他應用程序一樣,無論是否異步。換句話說,不一定要採取特殊的動作編寫響應服務器的方法,只需要改變表單,讓用戶訪問另一個 URL 或者做響應服務器需要的任何事情。這一節我們重點討論對服務器的響應和一種典型的動作 —— 即時改變用戶看到的表單中的一部分。
現在我們已經看到如何告訴服務器完成後應該做什麼:將 XMLHttpRequest
對象的 onreadystatechange
屬性設置爲要運行的函數名。這樣,當服務器處理完請求後就會自動調用該函數。也不需要擔心該函數的任何參數。我們從一個簡單的方法開始,如 清單 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> |
它僅僅發出一些簡單的警告,告訴您服務器什麼時候完成了任務。在自己的網頁中試驗這些代碼,然後在瀏覽器中打開(如果希望查看該例中的 XHTML,請參閱 清單 8)。輸入電話號碼然後離開該字段,將看到一個彈出的警告窗口(如 圖 3 所示),但是點擊 OK 又出現了……
根據瀏覽器的不同,在表單停止彈出警告之前會看到兩次、三次甚至四次警告。這是怎麼回事呢?原來我們還沒有考慮 HTTP 就緒狀態,這是請求/響應循環中的一個重要部分。
前面提到,服務器在完成請求之後會在 XMLHttpRequest
的 onreadystatechange
屬性中查找要調用的方法。這是真的,但還不完整。事實上,每當 HTTP 就緒狀態改變時它都會調用該方法。這意味着什麼呢?首先必須理解 HTTP 就緒狀態。
HTTP 就緒狀態表示請求的狀態或情形。它用於確定該請求是否已經開始、是否得到了響應或者請求/響應模型是否已經完成。它還可以幫助確定讀取服務器提供的響應文本或數據是否安全。在 Ajax 應用程序中需要了解五種就緒狀態:
- 0:請求沒有發出(在調用
open()
之前)。 - 1:請求已經建立但還沒有發出(調用
send()
之前)。 - 2:請求已經發出正在處理之中(這裏通常可以從響應得到內容頭部)。
- 3:請求已經處理,響應中通常有部分數據可用,但是服務器還沒有完成響應。
- 4:響應已完成,可以訪問服務器響應並使用它。
與大多數跨瀏覽器問題一樣,這些就緒狀態的使用也不盡一致。您也許期望任務就緒狀態從 0 到 1、2、3 再到 4,但實際上很少是這種情況。一些瀏覽器從不報告 0 或 1 而直接從 2 開始,然後是 3 和 4。其他瀏覽器則報告所有的狀態。還有一些則多次報告就緒狀態 1。在上一節中看到,服務器多次調用 updatePage()
,每次調用都會彈出警告框 —— 可能和預期的不同!
對於 Ajax 編程,需要直接處理的惟一狀態就是就緒狀態 4,它表示服務器響應已經完成,可以安全地使用響應數據了。基於此,回調方法中的第一行應該如 清單 13 所示。
function updatePage() {
if (request.readyState == 4)
alert("Server is done!");
} |
修改後就可以保證服務器的處理已經完成。嘗試運行新版本的 Ajax 代碼,現在就會看到與預期的一樣,只顯示一次警告信息了。
雖然 清單 13 中的代碼看起來似乎不錯,但是還有一個問題 —— 如果服務器響應請求並完成了處理但是報告了一個錯誤怎麼辦?要知道,服務器端代碼應該明白它是由 Ajax、JSP、普通 HTML 表單或其他類型的代碼調用的,但只能使用傳統的 Web 專用方法報告信息。而在 Web 世界中,HTTP 代碼可以處理請求中可能發生的各種問題。
比方說,您肯定遇到過輸入了錯誤的 URL 請求而得到 404 錯誤碼的情形,它表示該頁面不存在。這僅僅是 HTTP 請求能夠收到的衆多錯誤碼中的一種(完整的狀態碼列表請參閱 參考資料 中的鏈接)。表示所訪問數據受到保護或者禁止訪問的 403 和 401 也很常見。無論哪種情況,這些錯誤碼都是從完成的響應 得到的。換句話說,服務器履行了請求(即 HTTP 就緒狀態是 4)但是沒有返回客戶機預期的數據。
因此除了就緒狀態外,還需要檢查 HTTP 狀態。我們期望的狀態碼是 200,它表示一切順利。如果就緒狀態是 4 而且狀態碼是 200,就可以處理服務器的數據了,而且這些數據應該就是要求的數據(而不是錯誤或者其他有問題的信息)。因此還要在回調方法中增加狀態檢查,如 清單 14 所示。
function updatePage() {
if (request.readyState == 4)
if (request.status == 200)
alert("Server is done!");
} |
爲了增加更健壯的錯誤處理並儘量避免過於複雜,可以增加一兩個狀態碼檢查,請看一看 清單 15 中修改後的 updatePage()
版本。
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 看看會發生什麼。應該會看到警告信息說明要求的 URL 不存在 —— 好極了!很難處理所有的錯誤條件,但是這一小小的改變能夠涵蓋典型 Web 應用程序中 80% 的問題。
現在可以確保請求已經處理完成(通過就緒狀態),服務器給出了正常的響應(通過狀態碼),最後我們可以處理服務器返回的數據了。返回的數據保存在 XMLHttpRequest
對象的 responseText
屬性中。
關於 responseText
中的文本內容,比如格式和長度,有意保持含糊。這樣服務器就可以將文本設置成任何內容。比方說,一種腳本可能返回逗號分隔的值,另一種則使用管道符(即 |
字符)分隔的值,還有一種則返回長文本字符串。何去何從由服務器決定。
在本文使用的例子中,服務器返回客戶的上一個訂單和客戶地址,中間用管道符分開。然後使用訂單和地址設置表單中的元素值,清單 16 給出了更新顯示內容的代碼。
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, " |
首先,得到 responseText
並使用 JavaScript split()
方法從管道符分開。得到的數組放到 response
中。數組中的第一個值 —— 上一個訂單 —— 用 response[0]
訪問,被設置爲 ID 爲 “order” 的字段的值。第二個值 response[1]
,即客戶地址,則需要更多一點處理。因爲地址中的行用一般的行分隔符(“/n”字符)分隔,代碼中需要用 XHTML 風格的行分隔符 <br />
來代替。替換過程使用 replace()
函數和正則表達式完成。最後,修改後的文本作爲 HTML 表單 div
中的內部 HTML。結果就是表單突然用客戶信息更新了,如圖 4 所示。
結束本文之前,我還要介紹 XMLHttpRequest
的另一個重要屬性 responseXML
。如果服務器選擇使用 XML 響應則該屬性包含(也許您已經猜到)XML 響應。處理 XML 響應和處理普通文本有很大不同,涉及到解析、文檔對象模型(DOM)和其他一些問題。後面的文章中將進一步介紹 XML。但是因爲 responseXML
通常和 responseText
一起討論,這裏有必要提一提。對於很多簡單的 Ajax 應用程序 responseText
就夠了,但是您很快就會看到通過 Ajax 應用程序也能很好地處理 XML。