ajax|web
頁面重載提出了一個在Web應用開發中最大的可用性障礙,對於Java開發來說也是一個重大的挑戰。在本系列中,作者Philip McCarthy介紹了通過後台通道的方法來建立動態Web應用的經驗。
Ajax(Asynchronous JavaScript and XML)是一個結合了Java技術、XML、以及JavaScript的編程技術,可以讓你構建基於Java技術的Web應用,並打破了使用頁面重載的慣例。
Ajax,非同步JavaScript與XML,是使用用戶端指令碼與Web伺服器交換資料的Web應用開發方法。這樣,Web頁面不用打斷互動流程進行重新加裁,就可以動態地更新。使用Ajax,你可以建立接近本地案頭應用的,直接的、高可用的、更豐富的、更動態Web使用者介面介面。
Ajax不是一個技術,它更像是一個模式—標誌並描述有用的設計技巧的一種方法。對於剛瞭解它的許多開發人員來說,它是一種新的感覺,但是實現Ajax的所有組件都已存在了許多年。
當前的熱鬧是因為在2004與2005年出現了一些基於Ajax的非常動態WebUI,尤其是Google的GMail與Maps應用系統、與照片分享網站Flickr。這些UI充分地使用了後台通道,也被一些開發人員稱為“Web 2.0”,並導致了大家對Ajax應用興趣的猛漲。
在本系列中,我將給出所有你需要的開發你自己的Ajax應用的工具。在這第一篇文章中,我將解釋在Ajax背後的概念,示範為基於Web的應用系統建立一個Ajax介面的基本步驟。我將使用範例程式碼來示範實現Ajax動態介面的伺服器端Java代碼與用戶端JavaScript指令碼。最後,我將指出一些Ajax方法中易犯的錯誤,以及在建立Ajax應用時應該考慮的廣泛範圍內的可用性與易訪問性方面的問題。
一個更好的購物車
你可以使用Ajax來加強傳統的Web應用,通過消除頁面載入來使互動更流暢。為了示範它,我將使用一個簡單的,能動態更新加入的物品購物車。結合一個線上商店,這個方法可以不用等待點擊後的頁面重載,而讓我們繼續瀏覽並挑選物品到購物車中。
雖然,本文中的代碼針對購物車例子,但其中展示的技術可以用到其它的Ajax應用中。列表1中展示了購物車樣本所使用的HTML代碼。在整篇文章中,我都將會引用到這些HTML代碼。
Ajax處理過程
一個Ajax互動從一個稱為XMLHttpRequest的JavaScript對象開始。如同名字所暗示的,它允許一個用戶端指令碼來執行HTTP請求,並且將會解析一個XML格式的伺服器響應。Ajax處理過程中的第一步是建立一個XMLHttpRequest執行個體。使用HTTP方法(GET或POST)來處理請求,並將目標URL設定到XMLHttpRequest對象上。
現在,記住Ajax如何首先處於非同步處理狀態?當你發送HTTP請求,你不希望瀏覽器掛起並等待伺服器的響應,取而代之的是,你希望通過頁面繼續響應使用者的介面互動,並在伺服器響應真正到達後處理它們。
要完成它,你可以向XMLHttpRequest註冊一個回呼函數,並非同步地派發XMLHttpRequest請求。控制權馬上就被返回到瀏覽器,當伺服器響應到達時,回呼函數將會被調用。
在Java Web伺服器上,到達的請求與任何其它HttpServletRequest一樣。在解析請求參數後,servlet執行必需的應用邏輯,將響應序列化到XML中,並將它寫回HttpServletResponse。
回到用戶端,註冊在XMLHttpRequest上的回呼函數現在會被調用來處理由伺服器返回的XML文檔。最後,通過更新使用者介面來響應伺服器傳輸過來資料,使用JavaScript來操縱頁面的HTML DOM。圖1是Ajax處理過程的一個時序圖。
圖1:Ajax處理過程
現在,你應該對Ajax處理過程有了一個高層視圖。我將進入其中的每一步看看更細節的內容。如果你找不到自己的位置時,就回頭再看看圖1,加—因為Ajax方法的非同步本質,所以時序圖並不是筆直向前的。
發送一個XMLHttpRequest
我將從Ajax時序圖的起點開始:從瀏覽器建立並發送一個XMLHttpRequest。不幸的是,在不同的瀏覽器中建立XMLHttpRequest的方法都不一樣。列表2中樣本的JavaScript函數消除了這些與瀏覽器種類相關的問題,正確檢測與當前瀏覽器相關的方法,並返回一個可以使用的XMLHttpRequest。最好將它看成備用代碼,將它簡單拷貝到你的JavaScript庫中,在需要一個XMLHttpRequest時使用它即可。
列表2:跨瀏覽器建立一個XMLHttpRequest
/* * 返回一個建立的XMLHttpRequest對象, 若瀏覽器不支援則失敗*/function newXMLHttpRequest() { var xmlreq = false; if (window.XMLHttpRequest) { // 在非Microsoft瀏覽器中建立XMLHttpRequest對象 xmlreq = new XMLHttpRequest(); } else if (window.ActiveXObject) { //通過MS ActiveX建立XMLHttpRequest try { // 嘗試按新版InternetExplorer方法建立 xmlreq = new ActiveXObject ("Msxml2.XMLHTTP"); } catch (e1) { // 建立請求的ActiveX對象失敗 try { // 嘗試按老版InternetExplorer方法建立 xmlreq = new ActiveXObject("Microsoft.XMLHTTP"); } catch (e2) { // 不能通過ActiveX建立XMLHttpRequest } } } return xmlreq;} |
稍後,我將討論如何對待不支援XMLHttpReauest的瀏覽器的一些技巧。現在,列表2中展示的樣本函數將總是可以返回一個XMLHttpReauest執行個體。
回到購物車例子的情境中,只要使用者針對某一個目錄條目點擊了Add to Cart按鈕,我就要調用一個Ajax互動。名為addToCart()的onclick函數通過Ajax調用(如列表1中所示)來負責更新購物車的狀態。
在列表3中,addToCart()要做的第一件事就是通過調用newXMLHttpReauest函數(如列表2中所示)來擷取一個XMLHttpRequest的執行個體,並且註冊一個回呼函數來接受伺服器響應(我將在稍後詳細解釋,請參見列表6)。
因為,此請求將會修改伺服器狀態,我將使用一個HTTP POST來處理它。通過POST傳送資料需要三個步驟:首先,我需要開啟一個到進行通訊的伺服器資源的POST串連—在現在例子中是一個URL映射為cart.do的伺服器端servlet。
下一步,設定XMLHttpRequest的頭資訊,以標誌請求的內容為form-encoded。最後,將form-encoded資料作為請求體,並發送此請求。列表3中集中展示了這些步驟。
列表3:發送一個添加到購物車XMLHttpRequest
/* * 通過產品編碼,在購物車中添加一個條目 * itemCode – 需要添加條目的產品編碼 */function addToCart(itemCode){ // 擷取一個XMLHttpRequest執行個體 var req = newXMLHttpRequest(); // 設定用來從請求對象接收回調通知的控制代碼函數 var handlerFunction = getReadyStateHandler(req, updateCart); req.onreadystatechange = handlerFunction; // 開啟一個聯結到購物車servlet的 HTTP POST聯結 // 第三個參數表示請求是非同步 req.open("POST", "cart.do", true);// 指示請求體包含form資料 req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");// 發送標誌需要添加到購物車中條目的form-encoded資料req.send("action=add&item="+itemCode);} |
結合以上內容,你可以瞭解到Ajax處理過程的第一部分—就是在用戶端建立並發送HTTP請求。下一步是用來處理請求的Java Servlet代碼。
Servlet請求處理
通過一個servlet來處理XMLHttpRequest與處理一個來自瀏覽器的普通的HTTP請求基本上相似。可以通過調用HttpServletRequest.getParameter()來擷取由POST請求體傳送過來的form-encoded資料。
Ajax請求也與普通的WEB請求樣都成為此應用同一HttpSession會話進程的一部分。這對於購物車例子來說很有肜,因為我們可以通過會話將多個請求的狀態都儲存到同一個JavaBean購物車對象中,並可以序列化。
列表4是處理Ajax請求並更新購物車的簡單servlet的代碼片斷。從使用者會話中檢索出一個Cart Bean,並按請求的參數更新它。
之後Cart Bean被序列化到XML,並被寫回ServletRespone。注意,一定要將響應內容的類型設定為application/xml,否則,XMLHttpRequest將不能將響應內容解析為一個XML DOM。
列表4:處理Ajax請求的Servlet代碼
public void doPost(HttpServletRequest req,HttpServletResponse res)throws Java.io.IOException { Cart cart = getCartFromSession(req);String action = req.getParameter("action");String item = req.getParameter("item");if ((action != null)&&(item != null)) { // 在購物車中添加或移除一個條目 if ("add".equals(action)) { cart.addItem(item); } else if ("remove".equals(action)) { cart.removeItems(item); } } // 將購物車狀態序列化到XML String cartXml = cart.toXml(); // 將XML寫入response. res.setContentType("application/xml"); res.getWriter().write(cartXml);} |
列表5展示了由Cart.toXml()方法產生的XML。注意到產生的cart元素的屬性,是一個通過System.currentTimeMillis()產生的時間戳記。
列表5:Cart對象序列化得到的XML
<?xml version="1.0"?><cart generated="1123969988414" total="$171.95"> <item code="hat001"> <name>Hat</name> <quantity>2</quantity> </item> <item code="cha001"> <name>Chair</name> <quantity>1</quantity> </item> <item code="dog001"> <name>Dog</name> <quantity>1</quantity> </item></cart> |
如果你觀察一下下載網站提供的例子應用源碼中的Cart.Java,你將會看到它通過簡單地追加字串來產生XML。對於本例子來說,它已經足夠了,我將會在本系統文章的以後一期中介紹一些更好的方法。
現在你知道了CartServlet如何響應一個XMLHttpRequest。下一步是返回到用戶端,如何用伺服器響應來更新頁面狀態。
通過JavaScript來處理伺服器響應
XMLHttpRequest的readyState屬性是一個給出請求生命週期狀態的數字值。它從表示“未初始化”的0變化到表示“完成”的4。每次readyState改變時,都會引發readystatechange事件,通過onreadystatechange屬性配置回調處理函數將會被調用。
在列表3中,你已看到通過調用函數getReadyStateHandler()建立了一個處理函數,並被配置給onreadystatechange屬性。getReadyStateHandler()使用了這樣的事實:函數是JavaScript中的主要對象。
這意味著,函數可以作為參數被傳遞到其它函數,並且可以建立並返回其它函數。getReadystateHandler()要做是就是返回一個函數,來檢查XMLHttpRequet是否已經完成處理,並傳遞XML伺服器響應到由調用者指定的處理函數。列表6是getReadyStateHandler()的代碼。
列表6:函數getReadyStateHandler()
/* * Returns a function that waits for the specified XMLHttpRequest * to complete, then passes its XML response to the given handler function. * req - The XMLHttpRequest whose state is changing * responseXmlHandler - Function to pass the XML response to */function getReadyStateHandler(req,responseXmlHandler) { // 返回一個監聽XMLHttpRequest執行個體的匿名函數 return function () { // 如果請求的狀態是“完成” if (req.readyState == 4) { // 檢查是否成功接收了伺服器響應 if (req.status == 200) { // 將載有響應資訊的XML傳遞到處理函數 responseXmlHandler(req.responseXML); } else { // 有HTTP問題發生 alert("HTTP error: "+req.status); } } }} |
HTTP狀態代碼
在列表6中,XMLHttpRequest的status屬性被測試用來確定請求是否成功完成。當處理簡單的GET與POST請求,你可以認為只要不是200(OK)的狀態就表示發生了錯誤。若伺服器發送了一個重新導向響應(例如,301或302),瀏覽器會透明地完成重新導向並從新位置擷取相應的資源;XMLHttpRequest不會看到重新導向狀態代碼。
同時,瀏覽器自動添加一個緩衝控制:對於所有XMLHttpRequest都使用no-cache header,這樣用戶端代碼就可以不用處理304(not-modified)響應。
關於getReadyStateHandler()
getReadyStateHandler()是相對比較複雜的一段代碼,特別當你不能熟悉閱讀JavaScript時。折中方案是在你的JavaScript庫中包含此函數,你可以簡單地處理Ajax伺服器響應,而不用去注意XMLHttpRequest的內部細節。重要是你自己要理解在代碼中如何使用getReadyStateHandler()。
在列表3中,你看到getReadyStateHandler()被這樣調用:
handlerFunction=getReadyStateHandler(req,updateCart) |
由它返回的函數將會檢查在req變數中的XMLHttpRequest是否已完成,並調用由updateCart指定的回調方法處理響應XML。
提取購物車資料
列表7中展示了updateCart()中的代碼。此函數使用DOM來解析購物車XML文檔,並更新WEB頁面(參見列表1)來反映新的購物車內容。注意對用來提取資料的XML DOM的調用。
Cart元素上產生的屬性,即序列化時產生的時間戳記,通過檢測它可以保證不會用老的資料來覆蓋新的購物車資料。Ajax請求天生就是非同步,通過這個檢測可以有效避免在過程外到達的伺服器響應的幹擾。
列表7:更新頁面來反映出購物車XML文檔內容
function updateCart(cartXML) { // 從文檔中擷取根項目“cart” var cart = cartXML.getElementsByTagName("cart")[0]; // 保證此文檔是最新的 var generated = cart.getAttribute("generated"); if (generated > lastCartUpdate) { lastCartUpdate = generated;// 清除HTML列表,用來顯示購物車內容var contents =document.getElementById("cart-contents");contents.innerHTML = "";// 在購物車內按條目迴圈 var items = cart.getElementsByTagName("item"); for (var I = 0 ; I < items.length ; I++) { var item = items[I]; // 從name與quantity元素中提取文本節點 var name = item.getElementsByTagName("name") [0].firstChild.nodeValue; var quantity = item.getElementsByTagName ("quantity")[0].firstChild.nodeValue; // 為條目建立並添加到HTML列表中 var li = document.createElement("li"); li.appendChild (document.createTextNode(name+" x "+quantity)); contents.appendChild(li); } } // 更新購物車的金額累計 document.getElementById("total").innerHTML = cart.getAttribute("total");} |
到現在,關於Ajax處理過程的教程已經結束,也許你想讓應用運行起來,並看看它的實際運作。這個例子非常簡單,有非常大的改進的餘地。比如,我在伺服器端代碼中包含了從購物車中移除條目的代碼,但從用戶端UI中沒有訪問的途徑。作為一個練習,嘗試在現有的JavaScript基礎上實際這個功能。
使用Ajax的挑戰
與任何技術一樣,使用Ajax在相當多的方面都可能範錯誤。我在這兒討論的問題目前都缺少解決方案,並將會隨著Ajax的成熟而解決或提高。隨著開發Ajax應用經驗的不斷擷取,開發人員社區中將會出現最好的實踐經驗與指導方針。
XMLHttpRequest的有效性
Ajax開發人員面對的一個最大問題是當XMLHttpRequest不可用時如何反應。雖然大部分現代瀏覽器支援XMLHttpRequest,但還是有少量的使用者,他們的瀏覽器不能支援,或由於瀏覽器安全設定而阻止對XMLHttpRequest的使用。
若你的Web應用發佈於公司內部的Intranet上,你很可能可以指定支援哪種瀏覽器,並可以確保XMLHttpRequest是可用的。若你在公用WEB上發布,則你必須意識到由於假定XMLHttpRequest是可用的,所有就阻止了老瀏覽器、手持功能瀏覽器等等使用者來使用你的系統。
然而,你應該儘力保證應用系統“正常降級”使用,在系統中保留適用於不支援XMLHttpRequest的瀏覽器的功能。在購物車例子中,最好的方法是有一個Add to Cart按鈕,可以進行常規的提交處理,並重新整理頁面來反映購物車狀態的變化。
Ajax行衛可以在頁面被載入時通過JavaScript添加到頁面中,只在XMLHttpRequest可用的情況下,為每個Add to Cart按鈕加上JavaScript處理函數。另一個方法是在使用者登入時檢測XMLHttpRequest,再決定是提供Ajax版本還是常規基於form提交的版本。
可用性考慮
圍繞著Ajax應用的大部分問題都是很普通的問題。例如,讓使用者知道他們的輸入已經被註冊並處理,是很重要的,因為在XMLHttpRequest處理過程中並不能提供通常的漏鬥旋轉游標。一種方法是將“確認”按扭上的文本替換為“正在更新中…”,以避免使用者在等待響應時多次點擊按鈕。
另一個問題是,使用者可能沒有注意到他們正在觀看的頁面已經被更新。可以通過使用各種視覺技巧來將使用者的眼光吸引到頁面的更新地區。還有一個問題是通過Ajax更新頁面打斷了瀏覽器“退回前頁”按鈕的正常工作,地址欄中的URL不能反映頁面的全部狀態,並且不能使用書籤功能。參見Resource章節中列出的網站地址上的文章來瞭解更多Ajax應用關於可用性方面的問題。
伺服器負載
使用Ajax介面代替傳統的基於form的介面可能戲劇性地增加傳遞到伺服器的請求數量。例如,一個普通的Google搜尋給伺服器造成一次命中,並在使用者確認搜尋表單時發生。然而,Google Suggest,將會試圖自動完成你的搜尋字詞,在使用者打字時將會往伺服器發送多個請求。
在開發一個Ajax應用時,要注意到你將會發送多少請求到使用者器端,以及伺服器的負載指標。你可以通過在用戶端適當地緩衝請求、與伺服器響應來緩減負載壓力。你也應該在設計Ajax應用時盡量在用戶端處理更多的邏輯,而不用與伺服器端通訊。
處理非同步
一定要記住,沒有任何東西可以保證XMLHttpRequest將會按照它們被發送的順序來依次結束。實際上,你在設計系統時,腦子裡應該始終假定它們不會按原來順序結束。在購物車例子中,使用了一個最後更新的時間戳記來保證最新的資料不會被改寫。
這個非常基本的方法可以在購物車情境中工作,但可能不能在其它情況下工作。在設計時刻就要考慮你該如何處理非同步伺服器響應。
結論
你現在應該對於Ajax的基本原則有了一個良好的瞭解,另外,你應該理解一些更進階的隨Ajax方法而來的設計問題。建立一個成功的Ajax應用需要一系列的方法—從JavaScript UI設計到伺服器端架構—但是你現在應該已經具備了需要使用到的Ajax核心知識。