Ajax 為更好的 Web 應用程式鋪平了道路
在 Web 應用程式開發中,頁面重載迴圈是最大的一個使用障礙,對於 Java 開發人員來說也是一個嚴峻的挑戰。在這個系列中,作者 Philip McCarthy 介紹了一種建立Live App程式體驗的開創性方式。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)。這個非常基本的方式可以用於購物車情境,但是可能不適合其他情境。所以在設計時請考慮如何處理非同步伺服器響應。
結束語
現在您對 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)輕量級資料交換格式。
下載
描述 |
名字 |
大小 |
下載方法 |
Sample code |
j-ajax1.zip |
8 KB |
HTTP |
參考資料
學習
- 您可以參閱本文在 developerWorks 全球網站上的 英文原文。
- “Beyond the DOM”(Dethe Elza, developerWorks,2005 年 5 月):進行 XML 文檔訪問的有用的 JavaScript 技術。
- “AJAX 及使用 E4X 編寫 Web 服務指令碼,第 1 部分”(Paul Fremantle 和 Anthony Elder,developerWorks,2005 年 4 月):用 Ajax 在支援 E4X JavaScript 擴充的瀏覽器中進行 SOAP 調用。
- “Ajax: A New Approach to Web Applications”(Jesse James Garrett,Adaptive Path,2005 年 2 月):介紹 Ajax 起源的短文。
- The Java BluePrints Solutions Catalog:介紹了 Ajax 在幾個常見 Web 應用程式情境中的應用。
- AjaxPatterns.org:包含多項改進 Ajax 應用程式的 UI 技術。
- XMLHttpRequest Usability Guidelines:對使用 Ajax 提高使用者體驗的建議。
- Ajax Mistakes:Ajax 應用程式應當避免的可用性問題。
- Java 技術專區:在這裡可以找到關於 Java 編程的各個方面的文章。
獲得產品和技術
- Mozilla Firefox:DOM Inspector 和 JavaScript Debugger 擴充消除了許多 Ajax 開發的痛苦。