AJAX - XMLHttpRequest 的前生今世
【1】Web 2.0 一瞥
在深入研究程式碼之前首先看看最近的觀點 —— 一定要十分清楚 Web 2.0 這個概念。聽到 Web 2.0 這個詞的時候,應該首先問一問 “Web 1.0 是什麼?” 雖然很少聽人提到 Web 1.0,實際上它指的就是具有完全不同的請求和響應模型的傳統 Web。比如,到 Amazon.com 網站上點選一個按鈕或者輸入搜尋項。就會對伺服器傳送一個請求,然後響應再返回到瀏覽器。該請求不僅僅是圖書和書目列表,而是另一個完整的 HTML 頁面。因此當 Web 瀏覽器用新的 HTML 頁面重繪時,可能會看到閃爍或抖動。事實上,通過看到的每個新頁面可以清晰地看到請求和響應。
Web 2.0(在很大程度上)消除了這種看得見的往復互動。比如訪問 Google Maps 或 Flickr 這樣的站點(到這些支援 Web 2.0 和 Ajax 站點的連結請參閱 參考資料)。比如在 Google Maps 上,您可以拖動地圖,放大和縮小,只有很少的重繪操作。當然這裡仍然有請求和響應,只不過都藏到了幕後。作為使用者,體驗更加舒適,感覺很像桌面應用程式。這種新的感受和範型就是當有人提到 Web 2.0 時您所體會到的。
需要關心的是如何使這些新的互動成為可能。顯然,仍然需要發出請求和接收響應,但正是針對每次請求/響應互動的 HTML 重繪造成了緩慢、笨拙的 Web 互動的感受。因此很清楚,我們需要一種方法使傳送的請求和接收的響應只包含需要的資料而不是整個 HTML 頁面。惟一需要獲得整個新 HTML 頁面的時候就是希望使用者看到新頁面的時候。
但多數互動都是在已有頁面上增加細節、修改主體文字或者覆蓋原有資料。這些情況下,Ajax 和 Web 2.0 方法允許在不更新整個 HTML 頁面的情況下發送和接收資料。
【2】XMLHttpRequest 簡介
要真正實現這種絢麗的奇蹟,必須非常熟悉一個 JavaScript 物件,即 XMLHttpRequest。
下面給出將要用於該物件的很少的幾個方法和屬性:
open():建立到伺服器的新請求。 send():向伺服器傳送請求。 abort():退出當前請求。 readyState:提供當前 HTML 的就緒狀態。 responseText:伺服器返回的請求響應文字。
如果看到 XMLHttpRequest 的所有方法和屬性,就會發現它們都與非常簡單的請求/響應模型有關。
① 簡單的 new
首先需要建立一個新變數並賦給它一個 XMLHttpRequest 物件例項。這在 JavaScript 中很簡單,只要對該物件名使用 new 關鍵字即可。
建立新的 XMLHttpRequest 物件:
<script language="javascript" type="text/javascript">
var request = new XMLHttpRequest();
</script>
JavaScript 不要求指定變數型別,因此不需要像下面那樣做(在 Java 語言中可能需要這樣:
//建立 XMLHttpRequest 的 Java 虛擬碼
XMLHttpRequest request = new XMLHttpRequest();
因此在 JavaScript 中用 var 建立一個變數,給它一個名字(如 “request”),然後賦給它一個新的 XMLHttpRequest 例項。此後就可以在函式中使用該物件了。
② 錯誤處理
在實際上各種事情都可能出錯,而上面的程式碼沒有提供任何錯誤處理。較好的辦法是建立該物件,並在出現問題時優雅地退出。比如,任何較早的瀏覽器(不論您是否相信,仍然有人在使用老版本的 Netscape Navigator)都不支援 XMLHttpRequest,您需要讓這些使用者知道有些地方出了問題。
如下示例說明如何建立該物件,以便在出現問題的時候發出 JavaScript 警告。
//建立具有錯誤處理能力的 XMLHttpRequest
<script language="javascript" type="text/javascript">
var request = false;
try {
request = new XMLHttpRequest();
} catch (failed) {
request = false;
}
if (!request)
alert("Error initializing XMLHttpRequest!");
</script>
一定要理解這些步驟:
- 建立一個新變數 request 並賦值 false。後面將使用 false 作為判定條件,它表示還沒有建立 XMLHttpRequest 物件。
- 增加 try/catch 塊:
- 嘗試建立 XMLHttpRequest 物件。
- 如果失敗(catch (failed))則保證 request 的值仍然為 false。
- 檢查 request 是否仍為 false(如果一切正常就不會是 false)。
- 如果出現問題(request 是 false)則使用 JavaScript 警告通知使用者出現了問題。
程式碼非常簡單,對大多數 JavaScript 和 Web 開發人員來說,真正理解它要比讀寫程式碼花更長的時間。現在已經得到了一段帶有錯誤檢查的 XMLHttpRequest 物件建立程式碼,還可以告訴您哪兒出了問題。
③應付 Microsoft
看起來似乎一切良好,至少在用 Internet Explorer 試驗這些程式碼之前是這樣的。
顯然有什麼地方不對勁,而 Internet Explorer 很難說是一種過時的瀏覽器,因為全世界有 70% 在使用 Internet Explorer。換句話說,如果不支援 Microsoft 和 Internet Explorer 就不會受到 Web 世界的歡迎!因此我們需要採用不同的方法處理 Microsoft 瀏覽器。
經驗證發現 Microsoft 支援 Ajax,但是其 XMLHttpRequest 版本有不同的稱呼。事實上,它將其稱為幾種 不同的東西。如果使用較新版本的 Internet Explorer,則需要使用物件 Msxml2.XMLHTTP,而較老版本的 Internet Explorer 則使用 Microsoft.XMLHTTP。我們需要支援這兩種物件型別(同時還要支援非 Microsoft 瀏覽器)。
增加對 Microsoft 瀏覽器的支援:
<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!");
</script>
很容易被這些花括號迷住了眼睛,因此下面分別介紹每一步:
- 建立一個新變數 request 並賦值 false。使用 false 作為判斷條件,它表示還沒有建立 XMLHttpRequest 物件。
- 增加 try/catch 塊:
- 嘗試建立 XMLHttpRequest 物件。
- 如果失敗(catch (trymicrosoft)):
- 嘗試使用較新版本的 Microsoft 瀏覽器建立 Microsoft 相容的物件(Msxml2.XMLHTTP)。
- 如果失敗(catch (othermicrosoft))嘗試使用較老版本的 Microsoft 瀏覽器建立 Microsoft 相容的物件(Microsoft.XMLHTTP)。
- 如果失敗(catch (failed))則保證 request 的值仍然為 false。
- 檢查 request 是否仍然為 false(如果一切順利就不會是 false)。
- 如果出現問題(request 是 false)則使用 JavaScript 警告通知使用者出現了問題。
這樣修改程式碼之後再使用 Internet Explorer 試驗,就應該看到已經建立的表單(沒有錯誤訊息)。
④ 靜態與動態
再看一看上面①②③的程式碼,注意,所有這些程式碼都直接巢狀在 script 標記中。像這種不放到方法或函式體中的 JavaScript 程式碼稱為靜態 JavaScript。就是說程式碼是在頁面顯示給使用者之前的某個時候執行。(雖然根據規範不能完全精確地 知道這些程式碼何時執行對瀏覽器有什麼影響,但是可以保證這些程式碼在使用者能夠與頁面互動之前執行。)這也是多數 Ajax 程式設計師建立 XMLHttpRequest 物件的一般方式。
就是說,也可以將這些程式碼放在一個方法中。
將 XMLHttpRequest 建立程式碼移動到方法中:
<script language="javascript" type="text/javascript">
var request;
function createRequest() {
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!");
}
</script>
如果按照這種方式編寫程式碼,那麼在處理 Ajax 之前需要呼叫該方法。
⑤ 使用 XMLHttpRequest 的建立方法
<script language="javascript" type="text/javascript">
var request;
function createRequest() {
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() {
createRequest();
// Do something with the request variable
}
</script>
此程式碼惟一的問題是推遲了錯誤通知,這也是多數 Ajax 程式設計師不採用這一方法的原因。假設一個複雜的表單有 10 或 15 個欄位、選擇框等,當用戶在第 14 個欄位(按照表單順序從上到下)輸入文字時要啟用某些 Ajax 程式碼。這時候執行 getCustomerInfo() 嘗試建立一個 XMLHttpRequest 物件,但(對於本例來說)失敗了。然後向用戶顯示一條警告,明確地告訴他們不能使用該應用程式。但使用者已經花費了很多時間在表單中輸入資料!這是非常令人討厭的,而討厭顯然不會吸引使用者再次訪問您的網站。
如果使用靜態 JavaScript,使用者在點選頁面的時候很快就會看到錯誤資訊。這樣也很煩人,是不是?可能令使用者錯誤地認為您的 Web 應用程式不能在他的瀏覽器上執行。不過,當然要比他們花費了 10 分鐘輸入資訊之後再顯示同樣的錯誤要好。因此,建議編寫靜態的程式碼,讓使用者儘可能早地發現問題。
⑥ 用 XMLHttpRequest 傳送請求-設定伺服器 URL
得到請求物件之後就可以進入請求/響應迴圈了。記住,XMLHttpRequest 惟一的目的是讓您傳送請求和接收響應。其他一切都是 JavaScript、CSS 或頁面中其他程式碼的工作:改變使用者介面、切換影象、解釋伺服器返回的資料。準備好 XMLHttpRequest 之後,就可以向伺服器傳送請求了。
Ajax 採用一種沙箱安全模型。因此,Ajax 程式碼(具體來說就是 XMLHttpRequest 物件)只能對所在的同一個域傳送請求。在本地機器上執行的程式碼只能對本地機器上的伺服器端指令碼傳送請求。如果讓 Ajax 程式碼在 www.breakneckpizza.com 上執行,則必須 www.breakneck.com 中執行的指令碼傳送請求。
- 設定伺服器 URL
首先要確定連線的伺服器的 URL。這並不是 Ajax 的特殊要求,但仍然是建立連線所必需的,顯然現在您應該知道如何構造 URL 了。多數應用程式中都會結合一些靜態資料和使用者處理的表單中的資料來構造該 URL。
如下所示的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>
表單如下:
<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>
還要注意,當用戶輸入電話號碼或者改變電話號碼時,將觸發 getCustomerInfo() 方法。該方法取得電話號碼並構造儲存在 url 變數中的 URL 字串。記住,由於 Ajax 程式碼是沙箱型的,因而只能連線到同一個域,實際上 URL 中不需要域名。該例中的指令碼名為 /cgi-local/lookupCustomer.php。最後,電話號碼作為 GET 引數附加到該指令碼中:“phone=” + escape(phone)。
escape() 方法,它用於轉義不能用明文正確傳送的任何字元。比如,電話號碼中的空格將被轉換成字元 %20,從而能夠在 URL 中傳遞這些字元。
可以根據需要新增任意多個引數。比如,如果需要增加另一個引數,只需要將其附加到 URL 中並用 “與”(&)字元分開 [第一個引數用問號(?)和指令碼名分開]。
⑦ 用 XMLHttpRequest 傳送請求-開啟請求
有了要連線的 URL 後就可以配置請求了。可以用 XMLHttpRequest 物件的 open() 方法來完成。該方法有五個引數:
- request-type:傳送請求的型別。典型的值是 GET 或 POST,但也可以傳送 HEAD 請求。
- url:要連線的 URL。
- asynch:如果希望使用非同步連線則為 true,否則為 false。該引數是可選的,預設為 true。
- username:如果需要身份驗證,則可以在此指定使用者名稱。該可選引數沒有預設值。
- password:如果需要身份驗證,則可以在此指定口令。該可選引數沒有預設值。
通常使用其中的前三個引數。事實上,即使需要非同步連線,也應該指定第三個引數為 “true”。這是預設值,但堅持明確指定請求是非同步的還是同步的更容易理解。
程式碼如下:
function getCustomerInfo() {
var phone = document.getElementById("phone").value;
var url = "/cgi-local/lookupCustomer.php?phone=" + escape(phone);
request.open("GET", url, true);
}
⑧ 用 XMLHttpRequest 傳送請求-傳送請求
一旦用 open() 配置好之後,就可以傳送請求了。幸運的是,傳送請求的方法的名稱要比 open() 適當,它就是 send()。
send() 只有一個引數,就是要傳送的內容。但是在考慮這個方法之前,回想一下前面已經通過 URL 本身傳送過資料了:
var url = "/cgi-local/lookupCustomer.php?phone=" + escape(phone);
雖然可以使用 send() 傳送資料,但也能通過 URL 本身傳送資料。事實上,GET 請求中,用 URL 傳送資料要容易得多。如果需要傳送安全資訊或 XML,可能要考慮使用 send() 傳送內容。如果不需要通過 send() 傳遞資料,則只要傳遞 null 作為該方法的引數即可。
程式碼如下:
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。
首先一定要理解這些程式碼中的流程。建立其請求然後發出請求。此外,因為是非同步請求,所以 JavaScript 方法(例子中的 getCustomerInfo())不會等待伺服器。因此程式碼將繼續執行,就是說,將退出該方法而把控制返回給表單。使用者可以繼續輸入資訊,應用程式不會等待伺服器。
這就提出了一個有趣的問題:伺服器完成了請求之後會發生什麼?答案是什麼也不發生,至少對現在的程式碼而言如此!顯然這樣不行,因此伺服器在完成通過 XMLHttpRequest 傳送給它的請求處理之後需要某種指示說明怎麼做。
現在 onreadystatechange 屬性該登場了。該屬性允許指定一個回撥函式。回撥允許伺服器反向呼叫 Web 頁面中的程式碼。它也給了伺服器一定程度的控制權,當伺服器完成請求之後,會檢視 XMLHttpRequest 物件,特別是 onreadystatechange 屬性。然後呼叫該屬性指定的任何方法。之所以稱為回撥是因為伺服器向網頁發起呼叫,無論網頁本身在做什麼。比方說,可能在使用者坐在椅子上手沒有碰鍵盤的時候呼叫該方法,但是也可能在使用者輸入、移動滑鼠、滾動螢幕或者點選按鈕時呼叫該方法。它並不關心使用者在做什麼。
這就是稱之為非同步的原因:使用者在一層上操作表單,而在另一層上伺服器響應請求並觸發 onreadystatechange 屬性指定的回撥方法。
程式碼如下:
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() 之前設定的。傳送請求之前必須設定該屬性,這樣伺服器在回答完成請求之後才能檢視該屬性。
現在我們已經看到如何告訴伺服器完成後應該做什麼:將 XMLHttpRequest 物件的 onreadystatechange 屬性設定為要執行的函式名。這樣,當伺服器處理完請求後就會自動呼叫該函式。也不需要擔心該函式的任何引數。
<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>
它僅僅發出一些簡單的警告,告訴您伺服器什麼時候完成了任務。
⑩ HTTP就緒狀態
在自己的網頁中試驗這些程式碼,然後在瀏覽器中開啟。輸入電話號碼然後離開該欄位,將看到一個彈出的警告視窗(如 圖 3 所示),但是點選 OK 又出現了……
根據瀏覽器的不同,在表單停止彈出警告之前會看到兩次、三次甚至四次警告。這是怎麼回事呢?原來我們還沒有考慮 HTTP 就緒狀態,這是請求/響應迴圈中的一個重要部分。
前面提到,伺服器在完成請求之後會在 XMLHttpRequest 的 onreadystatechange 屬性中查詢要呼叫的方法。這是真的,但還不完整。事實上,每當 HTTP 就緒狀態改變時它都會呼叫該方法。這意味著什麼呢?首先必須理解 HTTP 就緒狀態。
HTTP 就緒狀態表示請求的狀態或情形。它用於確定該請求是否已經開始、是否得到了響應或者請求/響應模型是否已經完成。它還可以幫助確定讀取伺服器提供的響應文字或資料是否安全。
在 Ajax 應用程式中需要了解五種就緒狀態:
0:請求沒有發出(在呼叫 open() 之前)。
1:請求已經建立但還沒有發出(呼叫 send() 之前)。`在這裡插入程式碼片`
2:請求已經發出正在處理之中(這裡通常可以從響應得到內容頭部)。
3:請求已經處理,響應中通常有部分資料可用,但是伺服器還沒有完成響應。
4:響應已完成,可以訪問伺服器響應並使用它。
與大多數跨瀏覽器問題一樣,這些就緒狀態的使用也不盡一致。對於 Ajax 程式設計,需要直接處理的惟一狀態就是就緒狀態 4,它表示伺服器響應已經完成,可以安全地使用響應資料了。
修改updatePage方法如下:
function updatePage() {
if (request.readyState == 4)
alert("Server is done!");
}
(11)HTTP 狀態碼
雖然⑩中的程式碼看起來似乎不錯,但是還有一個問題 —— 如果伺服器響應請求並完成了處理但是報告了一個錯誤怎麼辦?要知道,伺服器端程式碼應該明白它是由 Ajax、JSP、普通 HTML 表單或其他型別的程式碼呼叫的,但只能使用傳統的 Web 專用方法報告資訊。而在 Web 世界中,HTTP 程式碼可以處理請求中可能發生的各種問題。
比方說,您肯定遇到過輸入了錯誤的 URL 請求而得到 404 錯誤碼的情形,它表示該頁面不存在。這僅僅是 HTTP 請求能夠收到的眾多錯誤碼中的一種(參考HTTP狀態碼)。表示所訪問資料受到保護或者禁止訪問的 403 和 401 也很常見。無論哪種情況,這些錯誤碼都是從完成的響應得到的。換句話說,伺服器履行了請求(即 HTTP 就緒狀態是 4)但是沒有返回客戶機預期的資料。
因此除了就緒狀態外,還需要檢查 HTTP 狀態。我們期望的狀態碼是 200,它表示一切順利。如果就緒狀態是 4 而且狀態碼是 200,就可以處理伺服器的資料了,而且這些資料應該就是要求的資料(而不是錯誤或者其他有問題的資訊)。
因此還要在回撥方法中增加狀態檢查,如下所示:
function updatePage() {
if (request.readyState == 4)
if (request.status == 200)
alert("Server is done!");
}
為了增加更健壯的錯誤處理並儘量避免過於複雜,可以增加一兩個狀態碼檢查,如下所示:
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);
}
(12)讀取響應文字
現在可以確保請求已經處理完成(通過就緒狀態),伺服器給出了正常的響應(通過狀態碼),最後我們可以處理伺服器返回的資料了。返回的資料儲存在 XMLHttpRequest 物件的 responseText 屬性中。
關於 responseText 中的文字內容,比如格式和長度,有意保持含糊。這樣伺服器就可以將文字設定成任何內容。比方說,一種指令碼可能返回逗號分隔的值,另一種則使用管道符(即 | 字元)分隔的值,還有一種則返回長文字字串。何去何從由伺服器決定。
在本文使用的例子中,伺服器返回客戶的上一個訂單和客戶地址,中間用管道符分開。然後使用訂單和地址設定表單中的元素值。
程式碼如下:
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() 方法從管道符分開。得到的陣列放到 response 中。陣列中的第一個值 —— 上一個訂單 —— 用 response[0] 訪問,被設定為 ID 為 “order” 的欄位的值。第二個值 response[1],即客戶地址,則需要更多一點處理。因為地址中的行用一般的行分隔符(“\n”字元)分隔,程式碼中需要用 XHTML 風格的行分隔符來代替。替換過程使用 replace() 函式和正則表示式完成。最後,修改後的文字作為 HTML 表單 div 中的內部 HTML。結果就是表單突然用客戶資訊更新了。
參考博文:XMLHTTPRequest屬性和方法詳解。