面向 Java 開發人員的 Ajax: 構建動態的 Java 應用程序

面向 Java 開發人員的 Ajax: 構建動態的 Java 應用程序

Philip McCarthy , 軟件開發顧問, 獨立諮詢顧問

2005 年 10 月 20 日


在 Web 應用程序開發中,頁面重載循環是最大的一個使用障礙,對於 Java™ 開發人員來說也是一個嚴峻的挑戰。在這個系列中,作者 Philip McCarthy 介紹了一種創建動態應用程序體驗的開創性方式。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 οnclick="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)輕量級數據交換格式。
發佈了25 篇原創文章 · 獲贊 1 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章