面向Java开发人员的Ajax: 构建动态的Java应用程序

日期: 2008-07-09 作者:Philip McCarthy 来源:TechTarget中国

  在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 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);
        }
      }
    }
  }
 
  关于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)轻量级数据交换格式。

我们一直都在努力坚持原创.......请不要一声不吭,就悄悄拿走。

我原创,你原创,我们的内容世界才会更加精彩!

【所有原创内容版权均属TechTarget,欢迎大家转发分享。但未经授权,严禁任何媒体(平面媒体、网络媒体、自媒体等)以及微信公众号复制、转载、摘编或以其他方式进行使用。】

微信公众号

TechTarget微信公众号二维码

TechTarget

官方微博

TechTarget中国官方微博二维码

TechTarget中国

相关推荐