ajax|基礎教程
一. 什麼是AJAX?
這個名字代表了非同步JavaScript+XMLHTTPRequest,並且意味著你可以在基於瀏覽器的JavaScript和伺服器之間建立通訊端通訊。其實AJAX並不是一種新技術,而是已經成功地用於現代瀏覽器中的若干成功技術的可能性組合。所有的AJAX應用程式實現了一種“豐富的”UI——這是通過JavaScript操作HTML文件物件模型並且經由XMLHttpRequest實現的精確定位的資料檢索來實現的。典型的樣本AJAX應用程式是Google Labs(http://labs.google.com)的Google Maps和Google Suggest。這些應用程式現場監視使用者輸入並且提供即時的頁面更新。最重要的是,在使用者通過地圖導航或輸入一個尋找字串的同時,這些事件不需要重新整理頁面。
事實上,支援這些令人感到驚訝的應用的技術已經出現一段時間了,儘管它們要求複雜的技能以及使用瀏覽器的技巧。一些專利產品就提供了相似的能力——如Macromedia Flash外掛程式,Java Applets或.NET運行時——在達到實用上已經有一段時間了。把一種可與伺服器通話的指令碼組件引入到瀏覽器中的思想早在IE 5.0中就已經存在。Firefox和其它流行的瀏覽器也加入到瀏覽器大軍中並以一種內建對象形式支援XMLHTTPRequest。隨著跨平台瀏覽器的出現,這些技術得到了認可並在2004年3月一家稱為Adaptive Path的公司中正式提出了AJAX。
簡而言之,由於來自於Google的支援和安裝了一點可用的瀏覽器技術,加上為了一種"更好的使用者體驗",每個人都在把用戶端技術添加到Web應用程式上。
二. AJAX與傳統應用程式的區別
一個傳統Web應用程式模型實際上是一種基本的事件——使用者被迫提交表單以實現頁面交換。也就是說,表單提交和頁面傳送無法得到保證:還有更壞的情形——使用者需要再次點擊。這與AJAX截然不同-——資料跨過線路而不是完整的HTML頁面傳輸。這種資料交換是經由特定的瀏覽器對象:XMLHttpRequest實現的;再由適當的邏輯來處理每個資料請求的結果,頁面的特定地區而不是完整的頁面被更新。結果是更快的速度,更少的擁擠和更好的資訊傳送控制。
傳統型"click-refresh"Web應用程式強迫使用者中斷工作過程而等待頁面的重裝。通過引入AJAX技術,一個用戶端指令碼能夠非同步地與伺服器通話,而使用者仍能保持輸入資料。除了對使用者透明之外,這樣的非同步意味著伺服器可以有更多時間來處理請求。
傳統Web應用程式把所有的處理代理到伺服器並且強迫伺服器進行狀態管理。AJAX允許靈活劃分應用程式邏輯以及客戶和伺服器之間的狀態管理。這就消除了一種"click-refresh"依賴性並且提供更好的伺服器延展性。當該狀態儲存在用戶端,你就不必跨越伺服器來維持會話或儲存/結束狀態-其使用到期日是由用戶端來定義的。
三. AJAX——分布式的MVC
儘管AJAX應用程式依靠JavaScript來實現描述層,然而處理能力和知識庫仍然存在於伺服器上。此時,AJAX應用程式大量的與J2EE伺服器通訊——把資料輸入/輸出Web服務和servlets。具有基於AJAX的描述層的J2EE應用程式和標準J2EE應用程式之間的區別首先在於,MVC是通過線路分布的。通過使用AJAX,視圖是本地的,而模型和控制器是分布式的——這使得開發人員能夠靈活地決定哪些組件會是基於用戶端的。具體地說,本地視圖通過巧妙地操作HTML DOM而產生圖形;控制器局部地處理使用者輸入並且根據開發人員的判斷擴充到伺服器的處理——經由HTTP請求(Web服務,XML/RPC或其它)實現;模型的遠程部分是根據用戶端需要而下載的以達到即時更新用戶端頁面;並且狀態是在用戶端收集的。
在以後的AJAX文章中,我們將比較深入地討論這裡的每一種組件並提供有關它們聯合在一起進行應用的樣本。現在,先不多說,讓我們詳細地分析一個簡單的AJAX樣本。
四. 郵政區號校正和查詢
我們將建立一個包含三個INPUT欄位(Zip,City和State)的HTML頁面。我們將保證,只要使用者輸入郵政區號的前三個數字,該頁面上的欄位就會用第一個匹配的狀態值填充。一旦使用者輸入了所有五位郵政區號數,我們將立即決定和填充相應的城市。如果郵政區號無效(在伺服器的資料庫沒有找到),那麼我們將把郵政區號的邊界設定為紅色。這樣的可視化線索有助於使用者並且在現代瀏覽器中已經成為一種標準(作為一執行個體,當Firefox找到一個HTML頁面中的匹配關鍵字時,它會高亮與你在瀏覽器尋找域輸入的內容一致的部分)。
讓我們首先建立一個簡單的包含三個INPUT欄位的HTML:zip,city和state。請注意,一旦一個字元輸入進郵政區號欄位域中,即調用方法zipChanged()。JavaScript函數zipChanged()(見下)在當zip長度為3時調用函數updateState(),而在當zip長度為5時調用函數up-dateCity()。而updateCity()和updateState()把大部分的工作代理到另一個函數ask()。
Zip:<input id="zipcode" type="text" maxlength="5" onKeyUp="zipChanged()"
style="width:60"/>
City: <input id="city" disabled maxlength="32" style="width:160"/>
State:<input id="state" disabled maxlength="2" style="width:30"/>
<script src="xmlhttp.js"></script>
<script>
var zipField = null;
function zipChanged(){
zipField = document.getElementById("zipcode")
var zip = zipField.value;
zip.length == 3?updateState(zip):zip.length == 5?updateCity(zip):"";
}
function updateState(zip) {
var stateField = document.getElementById("state");
ask("resolveZip.jsp?lookupType=state&zip="+zip, stateField, zipField);
}
function updateCity(zip) {
var cityField = document.getElementById("city");
ask("resolveZip.jsp? lookupType=city&zip="+zip, cityField, zipField);
}
</script>
函數ask()與伺服器進行通訊並分配一個回呼函數來處理伺服器的響應(見下列代碼)。後面,我們將分析具有雙重特點的resolveZip.jsp的內容-它根據zip欄位中的字元數尋找city或state資訊。重要的是,ask()使用了具有非同步特點的XmlHttpRequest,這樣填充state和city欄位或著色zip欄位邊界就可以不必減慢資料入口而得以實現。首先,我們調用request.open()-它用伺服器開啟通訊端頻道,使用一個HTTP動詞(GET或POST)作為第一個參數並且以資料提供者的URL作為第二個參數。request.open()的最後一個參數被設定為true-它指示該請求的非同步特性。注意,該請求還沒有被提交。隨著對request.send()的調用,開始提交-這可以為POST提供任何必要的有效載荷。在使用非同步請求時,我們必須使用request.onreadystatechanged屬性來分配請求的回呼函數。(如果請求是同步的話,我們應該能夠在調用request.send之後立即處理結果,但是我們也有可能阻斷使用者,直到該請求完成為止。)
HTTPRequest = function () {
var xmlhttp=null;
try {
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
} catch (_e) {
try {
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
} catch (_E) { }
}
if (!xmlhttp && typeof XMLHttpRequest != 'undefined') {
try {
xmlhttp = new XMLHttpRequest();
} catch (e) {
xmlhttp = false;
}
}
return xmlhttp;
}
function ask(url, fieldToFill, lookupField) {
var http = new HTTPRequest();
http.open("GET", url, true);
http.onreadystatechange = function (){ handleHttpResponse(http, fieldToFill,lookupField)};
http.send(null);
}
function handleHttpResponse(http, fieldToFill, lookupField) {
if (http.readyState == 4) {
result = http.responseText;
if ( -1 != result.search("null") ) {
lookupField.style.borderColor = "red";
fieldToFill.value = "";
} else {
lookupField.style.borderColor = "";
fieldToFill.value = result;
}
}
}
為ask()所使用的HttpRequest()函數(見上)是一跨瀏覽器的XMLHTTPRequest的一個執行個體的構造器;稍後我們將分析它。到目前為止,請注意對於handleResponse()的調用是如何用一匿名函數封裝的-這個函數是function(){handleHttpResponse(http,fieldToFill, lookupField)}。
該函數的代碼是動態建立的並且在每次我們給http.onreadstatechange屬性賦值時被編譯。結果,JavaScript建立一個指向上下文(所有的變數都可以存取正在結束的方法-ask())的指標。這樣以來,匿名函數和handleResponse()就能夠被保證充分存取所有的上下文宿主的變數,直至到匿名函數的參考被記憶體回收站收集為止。換句話說,無論何時我們的匿名函數被調用,它都能無縫地參考request,fieldToFill和lookupField變數,就象它們是全域的一樣。而且,每次ask()調用都將建立環境的一個獨立拷貝,並且此時這些變數中儲存有該函數將結束時的值。
現在,讓我們分析一下函數handleResponse()。既然它能夠在請求處理的不同狀態下啟用,那麼該函數將忽略所有的情形-除了該請求處理完成之外-這相應於request.readyState屬性等於4("Completed")。此時,該函數讀取伺服器的響應文本。與它的名字所暗示的相反,XmlHttpRequest的輸入和輸出都不必限於XML格式。特別地,我們的resolveZip.jsp(見源碼中的列表1)返回普通文本。如果傳回值為"unknown",那麼該函數將假定郵政區號是無效的並且把尋找欄位(zip)邊界顏色置為紅色。否則,傳回值被用於填充欄位state或city,並且zip的邊界被賦予一種預設顏色。
XMLHttpRequest-傳輸對象
讓我們返回到我們的XMLHTTPRequest的跨瀏覽器實現。最後一個列表包含一個HttpRequest()函數-它向上相容於IE5.0和Mozilla 1.8/FireFox。為簡化起見,我們只建立一個微軟XMLHTTPRequest對象,而且如果建立失敗,我們假定它是Firefox/Mozilla。
該函數的核心是XMLHTTPRequest-這是一個本機瀏覽器對象,它為包括HTTP協議的任何東西與伺服器之間的通訊提供方便。它允許指定任何HTTP動詞,頭部和有效載荷,並且能夠以非同步或同步方式工作。不需要下載也不需要安裝任何外掛程式-儘管在IE的情形下,XMLHTTPRequest是一個整合到瀏覽器內部的ActiveX。因而,"Run ActiveX Control and Plugins"預設IE許可權應該正好適合使用它。
最重要的是,XMLHTTPRequest允許一個到伺服器的RPC風格的編程查詢而不需要任何頁面重新整理。它以一種可預測的,可控制的方式來實現此-提供了到HTTP協議的所有細節的完整存取-包括頭部和資料的任何定製格式。在以後的文章中,我們將向你展示其它一些業界協議-你可以在這些傳輸協議(如Web服務和XML-RPC)之上運行-它們極大地簡化大規模應用程式的開發和維護。
五.伺服器端邏輯
最後,伺服器端的resolveZip.jsp被從函數ask()中調用(見所附源碼中的列表1)。這個resolveZip.jsp在兩種由當前的郵政區號長度所區分的獨立的場所下被調用(見zipChanged()函數)。請求參數lookupType的值或者是state或者是city。為簡化起見,我們將假定,兩個檔案state.properties和city.properties都位於伺服器中C磁碟機的根目錄下。resolveZip.jsp邏輯負責用適當的預裝載的檔案返回尋找值。
我們的支援AJAX的頁面現在已經準備好了。
六.遠程指令碼技術-一種可選方法
一些更舊的AJAX實現是基於所謂的遠程指令碼技術。這種思想是,使用者的行為導致經由IFRAME對伺服器進行查詢,而伺服器用JavaScript作出響應,該指令碼一旦到達用戶端立即被執行。這與XMLHttpRequest方法相比存在較大的區別,在後者情況下,伺服器響應資料而用戶端解釋資料。其好處是這種解決方案支援更舊的瀏覽器。
基於IFRAME樣本的HTML部分(見所附源碼中的列表2)與我們在XMLHTTPRequest場合下所用的極相似,但是這次我們將引入另外一個IFRAME元素-controller:
Zip:<input id="zipcode" type="text" maxlength="5" onKeyUp="zipChanged()"
style="width:60" size="20"/>
City: <input id="city" disabled maxlength="32" style="width:160" size="20"/>
State:<input id="state" disabled maxlength="2" style="width:30" size="20"/>
<iframe id="controller" style="visibility:hidden;width:0;height:0"></iframe>
我們保持每次擊鍵都調用zipChanged()一次,但是這一次,從zipChanged()中被調用的函數ask()(見所附源碼中的列表3)負責設定IFRAME的src屬性,而不是調用一個XMLHTTPRequest:
function ask(url, fieldToFill, lookupField){
var controller = document.getElementById("controller");
controller.src= url+"&field="+fieldToFill.id+"&zip="+lookupField.id;
}
伺服器端邏輯由一個粗略的resolveZip.jsp(見所附源碼中的列表4)所描述。它與它的XMLHTTPRequest對應物相區別-它返回JavaScript語句,這些語句設定變數欄位lookup和city的全域值,而且一旦它到達瀏覽器即從全域視窗的執行內容中調用函數response()。
函數response()是一修改版本的handleResponse()-這一函數可以免於處理未完成的請求(詳見本文所附源碼中的列表2)。
七. 難題
為簡化起見,讓我們"俯看"一下在我們的範例程式碼中的一些重要的問題:
1.事實-XMLHTTPRequest對象執行個體和回呼函數調用在被使用以後並沒被破壞-在每次調用後這有可能導致記憶體流失。適當編寫的代碼應該破壞或重用對象池中的這些執行個體。而且,用戶端必須使用與伺服器軟體相同的對象管理技術。
2.在大多數情況下,錯誤往往得不到有效處理。例如,在方法ask()中對request.open()的調用可能引發一個異常,這是必須要捕獲和處理的,即使在瀏覽器中沒有設定JavaScript異常自動捕獲功能。而handleResponse()函數又是另外一個例子。它必須要為可能的伺服器端和通訊錯誤而檢查headers和responseText值。在發生錯誤的情況下,它必須儘力恢複並/或者報告錯誤。正確開發的AJAX應用程式要儘可能避免"提交"鬆散的資料,因為往往存線上路斷開和其它低級通訊的問題-所以這些程式必須建立一個強壯的和自恢複的架構為此提供支援。
3.當前伺服器端架構提供相當多的功能-它們可以與一種自由重新整理方法和諧相處。例如,讓我們考慮一個定製的在指定時間內的伺服器端認證的問題。在這種情況下,我們必須攔截到XMLHTTPRequest調用的安全系統響應,顯示登入螢幕,然後在使用者被認證後重新發出請求。
所有的這些問題只是一些典型的用低級API工作的任何應用程式代碼,而且所有這些問題都能被解決。好訊息是,解決這些問題所需要的技術十分相似於大多數Java開發技術,如Web服務,定製標籤和XML/XSLT。唯一的區別在於,現在這些技術以下列形式用於用戶端:
·Web服務-使用SOAP/REST/RPC等簡單通訊標準
·用戶端定製標籤-打包豐富的用戶端控制項並整合AJAX功能
·資料操作-基於XML和基於XSLT技術
八. 小結
AJAX方法能夠向人們提供一種與傳統型應用程式相同的豐富的互連網體驗。但是,我們必須有選擇地使用AJAX技術,如當你仍線上購物時,你絕對不想讓你的信用卡通過幕後處理就悄悄地開始付款。AJAX會成為一種持續的動力嗎?我們當然希望這樣。在過去的五年時間內我們一直在努力開發AJAX應用程式並且能證明它是健全並且很有效。然而,它要求一個開發人員必須精通大量技術而不是在傳統的"click-refresh"Web應用程式中所使用的那些。