Posted on 2006-09-15 23:27
Computerboy 閱讀(740)
評論(1) 編輯 收藏 所屬分類:
編程技術
本文系轉載!
面向 Java 開發人員的 Ajax: 構建動態的 Java 應用程序
在 Web
應用程序開發中,頁面重載循環是最大的一個使用障礙,對于 Java 開發人員來說也是一個嚴峻的挑戰。Ajax(異步 JavaScript 和
XML)是一種編程技術,它允許為基于 Java 的 Web 應用程序把 Java 技術、XML 和 JavaScript
組合起來,從而打破頁面重載的范式。
Ajax(即異步 JavaScript 和 XML)是一種 Web
應用程序開發的手段,它采用客戶端腳本與 Web 服務器交換數據。所以,不必采用會中斷交互的完整頁面刷新,就可以動態地更新 Web 頁面。使用
Ajax,可以創建更加豐富、更加動態的 Web 應用程序用戶界面,其即時性與可用性甚至能夠接近本機桌面應用程序。
Ajax
不是一項技術,而更像是一個 模式 —— 一種識別和描述有用的設計技術的方式。Ajax 是新穎的,因為許多開發人員才剛剛開始知道它,但是所有實現
Ajax 應用程序的組件都已經存在若干年了。它目前受到重視是因為在 2004 和 2005 年出現了一些基于 Ajax 技術的非常棒的動態
Web UI,最著名的就是 Google 的 GMail 和 Maps 應用程序,以及照片共享站點
Flickr。這些用戶界面具有足夠的開創性,有些開發人員稱之為“Web 2.0”,因此對 Ajax 應用程序的興趣飛速上升。
在這
個系列中,我將提供使用 Ajax 開發應用程序需要的全部工具 。在第一篇文章中,我將解釋 Ajax 背后的概念,演示為基于 Java 的
Web 應用程序創建 Ajax 界面的基本步驟。我將使用代碼示例演示讓 Ajax 應用程序如此動態的服務器端 Java 代碼和客戶端
JavaScript。最后,我將指出 Ajax 方式的一些不足,以及在創建 Ajax 應用程序時應當考慮的一些更廣的可用性和訪問性問題。
更好的購物車
可
以用 Ajax 增強傳統的 Web
應用程序,通過消除頁面裝入從而簡化交互。為了演示這一點,我采用一個簡單的購物車示例,在向里面添加項目時,它會動態更新。這項技術如果整合到在線商
店,那么用戶可以持續地瀏覽和向購物車中添加項目,而不必在每次點擊之后都等候完整的頁面更新。雖然這篇文章中的有些代碼特定于購物車示例,但是演示的技
術可以應用于任何 Ajax 應用程序。清單 1 顯示了購物車示例使用的有關 HTML 代碼,整篇文章中都會使用這個 HTML。
清單1. 購物車示例的有關片斷
<!-- Table of products from store's catalog, one row per item -->
<th>Name</th> <th>Description</th> <th>Price</th> <th></th>
...
<tr>
? <!-- Item details -->
? <td>Hat</td> <td>Stylish bowler hat</td> <td>$19.99</td>
? <td>
??? <!-- Click button to add item to cart via Ajax request -->
??? <button onclick="addToCart('hat001')">Add to Cart</button>
? </td>
</tr>
...
<!-- Representation of shopping cart, updated asynchronously -->
<ul id="cart-contents">
? <!-- List-items will be added here for each item in the cart -->
?
</ul>
<!-- Total cost of items in cart displayed inside span element -->
Total cost: <span id="total">$0.00</span>
Ajax 往返過程
Ajax
交互開始于叫作 XMLHttpRequest 的 JavaScript 對象。顧名思義,它允許客戶端腳本執行 HTTP 請求,并解析 XML
服務器響應。Ajax 往返過程的第一步是創建 XMLHttpRequest 的實例。在 XMLHttpRequest 對象上設置請求使用的
HTTP 方法(GET 或 POST)以及目標 URL。
現在,您還記得 Ajax 的第一個 a
是代表異步(asynchronous) 嗎?在發送 HTTP
請求時,不想讓瀏覽器掛著等候服務器響應。相反,您想讓瀏覽器繼續對用戶與頁面的交互進行響應,并在服務器響應到達時再進行處理。為了實現這個要求,可以
在 XMLHttpRequest 上注冊一個回調函數,然后異步地分派
XMLHttpRequest。然后控制就會返回瀏覽器,當服務器響應到達時,會調用回調函數。
在 Java Web 服務器上,請求同其他 HttpServletRequest 一樣到達。在解析了請求參數之后,servlet 調用必要的應用程序邏輯,把響應序列化成 XML,并把 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
/*
?* Returns a new XMLHttpRequest object, or false if this browser
?* doesn't support it
?*/
function newXMLHttpRequest() {
? var xmlreq = false;
? if (window.XMLHttpRequest) {
??? // Create XMLHttpRequest object in non-Microsoft browsers
??? xmlreq = new XMLHttpRequest();
? } else if (window.ActiveXObject) {
??? // Create XMLHttpRequest via MS ActiveX
??? try {
????? // Try to create XMLHttpRequest in later versions
????? // of Internet Explorer
????? xmlreq = new ActiveXObject("Msxml2.XMLHTTP");
??? } catch (e1) {
????? // Failed to create required ActiveXObject
????? try {
??????? // Try version supported by older versions
??????? // of Internet Explorer
??????? xmlreq = new ActiveXObject("Microsoft.XMLHTTP");
????? } catch (e2) {
??????? // Unable to create an XMLHttpRequest with ActiveX
????? }
??? }
? }
? return xmlreq;
}
?
稍后我將討論處理那些不支持 XMLHttpRequest 的瀏覽器的技術。目前,示例假設清單 2 的 newXMLHttpRequest 函數總能返回 XMLHttpRequest 實例。
返
回示例的購物車場景,我想要當用戶在目錄項目上點擊 Add to Cart 時啟動 Ajax 交互。名為 addToCart() 的
onclick 處理函數負責通過 Ajax 調用來更新購物車的狀態(請參閱 清單 1)。正如清單 3 所示,addToCart()
需要做的第一件事是通過調用清單 2 的 newXMLHttpRequest() 函數得到 XMLHttpRequest
對象。接下來,它注冊一個回調函數,用來接收服務器響應(我稍后再詳細解釋這一步;請參閱 清單 6)。
因為請求會修改服務器上的狀態,
所以我將用 HTTP POST 做這個工作。通過 POST 發送數據要求三個步驟。第一,需要打開與要通信的服務器資源的 POST 連接 ——
在這個示例中,服務器資源是一個映射到 URL cart.do 的 servlet。然后,我在 XMLHttpRequest
上設置一個頭,指明請求的內容是表單 編碼的數據。最后,我用表單編碼的數據作為請求體發送請求。
清單 3 把這些步驟放在了一起。
清單 3. 分派 Add to Cart XMLHttpRequest
/*
?* Adds an item, identified by its product code, to the shopping cart
?* itemCode - product code of the item to add.
?*/
function addToCart(itemCode) {
? // Obtain an XMLHttpRequest instance
? var req = newXMLHttpRequest();
? // Set the handler function to receive callback notifications
? // from the request object
? var handlerFunction = getReadyStateHandler(req, updateCart);
? req.onreadystatechange = handlerFunction;
?
? // Open an HTTP POST connection to the shopping cart servlet.
? // Third parameter specifies request is asynchronous.
? req.open("POST", "cart.do", true);
? // Specify that the body of the request contains form data
? req.setRequestHeader("Content-Type",
?????????????????????? "application/x-www-form-urlencoded");
? // Send form encoded data stating that I want to add the
? // specified item to the cart.
? req.send("action=add&item="+itemCode);
}
這就是建立 Ajax 往返過程的第一部分,即創建和分派來自客戶機的 HTTP 請求。接下來是用來處理請求的 Java servlet 代碼。
servlet 請求處理
用
servlet 處理 XMLHttpRequest,與處理普通的瀏覽器 HTTP 請求一樣??梢杂?
HttpServletRequest.getParameter() 得到在 POST 請求體中發送的表單編碼數據。Ajax
請求被放進與來自應用程序的常規 Web 請求一樣的 HttpSession
中。對于示例購物車場景來說,這很有用,因為這讓我可以把購物車狀態封裝在 JavaBean 中,并在請求之間在會話中維持這個狀態。
清
單 4 是處理 Ajax 請求、更新購物車的簡單 servlet 的一部分。Cart bean
是從用戶會話中獲得的,并根據請求參數更新它的狀態。然后 Cart 被序列化成 XML,XML 又被寫入
ServletResponse。重要的是把響應的內容類型設置為 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)) {
??? // Add or remove items from the Cart
??? if ("add".equals(action)) {
????? cart.addItem(item);
??? } else if ("remove".equals(action)) {
????? cart.removeItems(item);
??? }
? }
? // Serialize the Cart's state to XML
? String cartXml = cart.toXml();
? // Write XML to response.
? res.setContentType("application/xml");
? res.getWriter().write(cartXml);
}
清單 5 顯示了 Cart.toXml() 方法生成的示例 XML。它很簡單。請注意 cart 元素的 generated 屬性,它是 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 的方式只是把字符串添加在一起。雖然對這個示例來說足夠了,但是對于從 Java 代碼生成 XML 來說則是最差的方式。我將在這個系列的下一期中介紹一些更好的方式。
現在您已經知道了 CartServlet 響應 XMLHttpRequest 的方式。下一件事就是返回客戶端,查看如何用 XML 響應更新頁面狀態。
用 JavaScript 進行響應處理
XMLHttpRequest
的 readyState 屬性是一個數值,它指出請求生命周期的狀態。它從 0(代表“未初始化”)變化到 4(代表“完成”)。每次
readyState 變化時,readystatechange 事件就觸發,由 onreadystatechange
屬性指定的事件處理函數就被調用。
在 清單 3 中已經看到了如何調用 getReadyStateHandler()
函數創建事件處理函數。然后把這個事件處理函數分配給 onreadystatechange 屬性。getReadyStateHandler()
利用了這樣一個事實:函數是 JavaScript
中的一級對象。這意味著函數可以是其他函數的參數,也可以創建和返回其他函數。getReadyStateHandler()
的工作是返回一個函數,檢查 XMLHttpRequest 是否已經完成,并把 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) {
? // Return an anonymous function that listens to the
? // XMLHttpRequest instance
? return function () {
??? // If the request's status is "complete"
??? if (req.readyState == 4) {
???? ?
????? // Check that a successful server response was received
????? if (req.status == 200) {
??????? // Pass the XML payload of the response to the
??????? // handler function
??????? responseXmlHandler(req.responseXML);
????? } else {
??????? // An HTTP problem has occurred
??????? alert("HTTP error: "+req.status);
????? }
??? }
? }
}
HTTP 狀態碼
在
清單 6 中,檢查 XMLHttpRequest 的 status 屬性以查看請求是否成功完成。status 包含服務器響應的 HTTP
狀態碼。在執行簡單的 GET 和 POST 請求時,可以假設任何大于 200 (OK)的碼都是錯誤。如果服務器發送重定向響應(例如 301 或
302),瀏覽器會透明地進行重定向并從新的位置獲取資源;XMLHttpRequest 看不到重定向狀態碼。而且,瀏覽器會自動添加
Cache-Control: no-cache 頭到所有 XMLHttpRequest,這樣客戶代碼永遠也不用處理
304(未經修改)服務器響應。
關于 getReadyStateHandler()
getReadyStateHandler()
是段相對復雜的代碼,特別是如果您不習慣閱讀 JavaScript 的話。但是通過把這個函數放在 JavaScript 庫中,就可以處理
Ajax 服務器響應,而不必處理 XMLHttpRequest 的內部細節。重要的是要理解如何在自己的代碼中使用
getReadyStateHandler()。
在 清單 3 中看到了 getReadyStateHandler()
像這樣被調用:handlerFunction = getReadyStateHandler(req,
updateCart)。在這個示例中,getReadyStateHandler() 返回的函數將檢查在 req 變量中的
XMLHttpRequest 是否已經完成,然后用響應的 XML 調用名為 updateCart 的函數。
提取購物車數據
清
單 7 是 updateCart() 本身的代碼。函數用 DOM 調用檢查購物車的 XML 文檔,然后更新 Web 頁面(請參閱 清單
1),反映新的購物車內容。這里的重點是用來從 XML DOM 提取數據的調用。cart 元素的 generated 屬性是在 Cart
序列化為 XML 時生成的一個時間戳,檢查它可以保證新的購物車數據不會被舊的數據覆蓋。Ajax
請求天生是異步的,所以這個檢查可以處理服務器響應未按次序到達的情況。
清單 7. 更新頁面,反映購物車的 XML 文檔
function updateCart(cartXML) {
?// Get the root "cart" element from the document
?var cart = cartXML.getElementsByTagName("cart")[0];
?// Check that a more recent cart document hasn't been processed
?// already
?var generated = cart.getAttribute("generated");
?if (generated > lastCartUpdate) {
?? lastCartUpdate = generated;
?? // Clear the HTML list used to display the cart contents
?? var contents = document.getElementById("cart-contents");
?? contents.innerHTML = "";
?? // Loop over the items in the cart
?? var items = cart.getElementsByTagName("item");
?? for (var I = 0 ; I < items.length ; I++) {
???? var item = items[I];
???? // Extract the text nodes from the name and quantity elements
???? var name = item.getElementsByTagName("name")[0]
?????????????????????????????????????????????? .firstChild.nodeValue;
????????????????????????????????????????????? ?
???? var quantity = item.getElementsByTagName("quantity")[0]
?????????????????????????????????????????????? .firstChild.nodeValue;
???? // Create and add a list item HTML element for this cart item
???? var li = document.createElement("li");
???? li.appendChild(document.createTextNode(name+" x "+quantity));
???? contents.appendChild(li);
?? }
?}
?// Update the cart's total using the value from the cart document
?document.getElementById("total").innerHTML =
????????????????????????????????????????? cart.getAttribute("total");
}
到
此,整個 Ajax 往返過程完成了,但是您可能想讓 Web 應用程序運行一下查看實際效果(請參閱
下載一節)。這個示例非常簡單,有很多需要改進之處。例如,我包含了從購物車中清除項目的服務器端代碼,但是無法從 UI
訪問它。作為一個好的練習,請試著在應用程序現有的 JavaScript 代碼之上構建出能夠實現這個功能的代碼。
使用 Ajax 的挑戰
就像任何技術一樣,使用 Ajax 也有許多出錯的可能性。我目前在這里討論的問題還缺乏容易的解決方案,但是會隨著 Ajax 的成熟而改進。隨著開發人員社區增加開發 Ajax 應用程序的經驗,將會記錄下最佳實踐和指南。
XMLHttpRequest 的可用性
Ajax
開發人員面臨的一個最大問題是:在沒有 XMLHttpRequest 可用時該如何響應?雖然主要的現代瀏覽器都支持
XMLHttpRequest,但仍然有少數用戶的瀏覽器不支持,或者瀏覽器的安全設置阻止使用 XMLHttpRequest。如果開發的 Web
應用程序要部署在企業內部網,那么可能擁有指定支持哪種瀏覽器的權力,從而可以認為 XMLHttpRequest 總能使用。但是,如果要部署在公共
Web 上,那么就必須當心,如果假設 XMLHttpRequest
可用,那么就可能會阻止那些使用舊的瀏覽器、殘疾人專用瀏覽器和手持設備上的輕量級瀏覽器的用戶使用您的應用程序。
所以,您應當努力讓應
用程序“平穩降級”,在沒有 XMLHttpRequest 支持的瀏覽器中也能夠工作。在購物車的示例中,把應用程序降級的最好方式可能是讓 Add
to Cart 按鈕執行一個常規的表單提交,刷新頁面來反映購物車更新后的狀態。Ajax 的行為應當在頁面裝入的時候就通過 JavaScript
添加到頁面,只有在 XMLHttpRequest 可用時才把 JavaScript 事件處理函數附加到每個 Add to Cart
按鈕。另一種方式是在用戶登錄時檢測 XMLHttpRequest 是否可用,然后相應地提供應用程序的 Ajax 版本或基于表單的普通版本。
可用性考慮
關
于 Ajax 應用程序的某些可用性問題比較普遍。例如,讓用戶知道他們的輸入已經注冊了可能是重要的,因為沙漏光標和 spinning
瀏覽器的常用反饋機制“throbber”對 XMLHttpRequest 不適用。一種技術是用“Now updating...”類型的信息替換
Submit 按鈕,這樣用戶在等候響應期間就不會反復單擊按鈕了。
另一個問題是,用戶可能沒有注意到他們正在查看的頁面的某一部分已經
更新了。可以使用不同的可視技術,把用戶的眼球帶到頁面的更新區域,從而緩解這個問題。由 Ajax
更新頁面造成的其他問題還包括:“破壞了”瀏覽器的后退按鈕,地址欄中的 URL 也無法反映頁面的整個狀態,妨礙了設置書簽。請參閱
參考資料一節,獲得專門解決 Ajax 應用程序可用性問題的文章。
服務器負載
用 Ajax
實現代替普通的基于表單的 UI,會大大提高對服務器發出的請求數量。例如,一個普通的 Google Web
搜索對服務器只有一個請求,是在用戶提交搜索表單時出現的。而 Google Suggest
試圖自動完成搜索術語,它要在用戶輸入時向服務器發送多個請求。在開發 Ajax
應用程序時,要注意將要發送給服務器的請求數量以及由此造成的服務器負荷。降低服務器負載的辦法是,在客戶機上對請求進行緩沖并且緩存服務器響應(如果可
能的話)。還應該嘗試將 Ajax Web 應用程序設計為在客戶機上執行盡可能多的邏輯,而不必聯絡服務器。
處理異步
非
常重要的是,要理解無法保證 XMLHttpRequest
會按照分派它們的順序完成。實際上,應當假設它們不會按順序完成,并且在設計應用程序時把這一點記在心上。在購物車的示例中,使用最后更新的時間戳來確保
新的購物車數據不會被舊的數據覆蓋(請參閱 清單
7)。這個非?;镜姆绞娇梢杂糜谫徫镘噲鼍埃强赡懿贿m合其他場景。所以在設計時請考慮如何處理異步的服務器響應。
結束語
現
在您對 Ajax 的基本原則應當有了很好的理解,對參與 Ajax 交互的客戶端和服務器端組件也應當有了初步的知識。這些是基于 Java 的
Ajax Web 應用程序的構造塊。另外,您應當理解了伴隨 Ajax 方式的一些高級設計問題。創建成功的 Ajax 應用程序要求整體考慮,從
UI 設計到 JavaScript 設計,再到服務器端架構;但是您現在應當已經武裝了考慮其他這些方面所需要的核心 Ajax 知識。
如
果使用這里演示的技術編寫大型 Ajax 應用程序的復雜性讓您覺得恐慌,那么有好消息給您。由于 Struts、Spring 和
Hibernate 這類框架的發展把 Web 應用程序開發從底層 Servlet API 和 JDBC 的細節中抽象出來,所以正在出現簡化
Ajax 開發的工具包。其中有些只側重于客戶端,提供了向頁面添加可視效果的簡便方式,或者簡化了對 XMLHttpRequest
的使用。有些則走得更遠,提供了從服務器端代碼自動生成 Ajax 接口的方式。這些框架替您完成了繁重的任務,所以您可以采用更高級的方式進行
Ajax 開發。我在這個系列中將研究其中的一些。
Ajax 社區正在快速前進,所以會有大量有價值的信息涌現。在閱讀這個系列的下一期之前,我建議您參考 參考資料 一節中列出的文章,特別是如果您是剛接觸 Ajax 或客戶端開發的話。您還應當花些時間研究示例源代碼并考慮一些增強它的方式。
在
這個系列的下一篇文章中,我將深入討論 XMLHttpRequest API,并推薦一些從 JavaBean 方便地創建 XML
的方式。我還將介紹替代 XML 進行 Ajax 數據傳遞的方式,例如 JSON(JavaScript Object
Notation)輕量級數據交換格式。