在 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. 购物车示例的有关片断
<!---->
Name Description Price
...
<!---->
Hat Stylish bowler hat $19.99
<!---->
Add to Cart
...
<!---->
<!---->
Total cost:
$0.00
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 序列化示例
<!---->
<cart total="$171.95" generated="1123969988414"></cart>
<item code="hat001"></item>
<name></name>Hat
<quantity></quantity>2
<item code="cha001"></item>
<name></name>Chair
<quantity></quantity>1
<item code="dog001"></item>
<name></name>Dog
<quantity></quantity>1
如果查看应用程序源代码(可以从 下载 一节得到)中的 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)轻量级数据交换格式。