ajax第四章
我们已经介绍了Ajax技术,也知道了如何使用XMLHttpRequest对象,现在要把它们结合起来,该怎么做呢?哪些情况下需要应用Ajax技术?当然,Ajax的潜力几乎是无穷尽的,关于Ajax的使用,灵感可能源源不断。本章将展示一些例子,在这些情况下,使用Ajax技术可以让应用突飞猛进。有些情况是一目了然的,有些则不是。不过无论怎样,对Ajax应用积累的经验越多,你就越会找到自己的方法来改善应用。在这些例子中,大多数都使用Java servlet作为服务器端组件,其实每个例子也都能很容易地使用.NET、Ruby、Perl、PHP或任何其他服务器端技术来编写。
4.1 完成验证
关于可用性有一句金玉良言,即防患于未然,根本杜绝错误的发生。但是如果真的出现了错误,你就要第一时间通知用户。在Ajax之前,基于Web的应用必须提交整个页面才能验证数据,或者要依赖复杂的JavaScript来检查表单。尽管有些检查确实很简单,可以使用JavaScript编写,但另外一些检查则不然,完全靠JavaScript编写是办不到的。当然,在客户端编写的每一个验证例程都必须在服务器上以某种方式重写,因为用户有可能禁用JavaScript。
利用Ajax,你不用再受这个限制,不再只是编写简单的客户端验证和重复的逻辑。现在,如果你想为用户提供更能体现交互性的体验,可以简单地调用为服务器编写的验证例程。在大多数情况下,这个逻辑编写起来更简单,测试也更容易,而且完全可以借助于现有的框架。
有人问,在应用中应该从哪里开始使用Ajax,我们一般会建议从验证开始。你很可能要去掉一些JavaScript,而且可以很容易地加入一些现有的服务器端逻辑。本节将介绍一个例子,这是最常见的验证之一:日期验证。
这个例子的HTML很简单(见代码清单4-1)。其中有一个标准的输入框,相应的onchange()事件(当然,可以使用你认为合适的任何事件)会触发验证方法。可以看到,要调用标准的createXMLHttpRequest()方法,然后把输入值发送到Valid
ationServlet servlet。callback()函数从服务器得到结果,然后委托给setMessage ()方法,这个方法会检查值以确定用什么颜色显示消息。
代码清单4-1validation.html
var xmlHttp;
function createXMLHttpRequest() {
if (window.ActiveXObject) {
xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
}
else if (window.XMLHttpRequest) {
xmlHttp = new XMLHttpRequest();
}
}
function validate() {
createXMLHttpRequest();
var date = document.getElementById("birthDate");
var url = "ValidationServlet?birthDate=" + escape(date.value);
xmlHttp.open("GET", url, true);
xmlHttp.onreadystatechange = callback;
xmlHttp.send(null);
}
function callback() {
if (xmlHttp.readyState == 4) {
if (xmlHttp.status == 200) {
var mes =
xmlHttp.responseXML
.getElementsByTagName("message")[0].firstChild.data;
var val =
xmlHttp.responseXML
.getElementsByTagName("passed")[0].firstChild.data;
setMessage(mes, val);
}
}
}
function setMessage(message, isValid) {
var messageArea = document.getElementById("dateMessage");
var fontColor = "red";
if (isValid == "true") {
fontColor = "green";
}
messageArea.innerHTML = ""
+ message + " ";
}
Ajax Validation Example
Birth date:
服务器端代码也很简单(见代码清单4-2)。为简单起见,这里把验证代码放在servlet中,而在生产环境中很可能会把验证代码委托给验证服务。
代码清单4-2ValidationServlet.java
package ajaxbook.chap4;
import java.io.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import javax.servlet.*;
import javax.servlet.http.*;
public class ValidationServlet extends HttpServlet {
/** Handles the HTTP GET
method.
* @param request servlet request
* @param response servlet response
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
PrintWriter out = response.getWriter();
boolean passed = validateDate(request.getParameter("birthDate"));
response.setContentType("text/xml");
response.setHeader("Cache-Control", "no-cache");
String message = "You have entered an invalid date.";
if (passed) {
message = "You have entered a valid date.";
}
out.println("
out.println("
out.println("
out.println("");
out.close();
}
/**
* Checks to see whether the argument is a valid date.
* A null date is considered invalid. This method
* used the default data formatter and lenient
* parsing.
*
* @param date a String representing the date to check
* @return message a String representing the outcome of the check
*/
private boolean validateDate(String date) {
boolean isValid = true;
if(date != null) {
SimpleDateFormat formatter= new SimpleDateFormat("MM/dd/yyyy");
try {
formatter.parse(date);
} catch (ParseException pe) {
System.out.println(pe.toString());
isValid = false;
}
} else {
isValid = false;
}
return isValid;
}
}
运行这个例子会得到图4-1和图4-2所示的结果。
图4-1输入非法的日期
图4-2输入合法的日期
4.2 读取响应首部
你有时可能需要从服务器获取一些内容,例如,可能想“ping”一下服务器,验证服务器是否正常运行。此时,你也许只想读取服务器发出的响应首部,而忽略内容。通过读取响应首部,可以得出Content-Type(内容类型)、Content-Lengt h(内容长度),甚至Last- Modified(最后一次修改)的日期。
如果只关注响应首部,完成这样一个请求的标准做法是使用HEAD请求,而不是前面讨论的GET或POST请求。当服务器对HEAD请求做出响应时,它只发送响应首部而忽略内容,即使可以向浏览器返回所请求的内容,也不会真的把内容返回。由于忽略了内容,对HEAD请求的响应比对GET或POST的响应就小得多。
代码清单4-3展示了从XMLHttpRequest对象获取响应首部的多种方法,并介绍了这些方法在实际中的使用。这个页面有4个链接,分别对应从XMLHttpRe quest对象读取响应首部的各个方法。
代码清单4-3readingResponseHeaders.html
"https://www.360docs.net/doc/2219284619.html,/TR/xhtml1/DTD/xhtml1-strict.dtd">
var xmlHttp;
var requestType = "";
function createXMLHttpRequest() {
if (window.ActiveXObject) {
xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
}
else if (window.XMLHttpRequest) {
xmlHttp = new XMLHttpRequest();
}
}
function doHeadRequest(request, url) {
requestType = request;
createXMLHttpRequest();
xmlHttp.onreadystatechange = handleStateChange;
xmlHttp.open("HEAD", url, true);
xmlHttp.send(null);
}
function handleStateChange() {
if(xmlHttp.readyState == 4) {
if(requestType == "allResponseHeaders") {
getAllResponseHeaders();
}
else if(requestType == "lastModified") {
getLastModified();
}
else if(requestType == "isResourceAvailable") { getIsResourceAvailable();
}
}
}
function getAllResponseHeaders() {
alert(xmlHttp.getAllResponseHeaders());
}
function getLastModified() {
alert("Last Modified: " + xmlHttp.getResponseHeader("Last-Modified"));
}
function getIsResourceAvailable() {
if(xmlHttp.status == 200) {
alert("Successful response");
}
else if(xmlHttp.status == 404) {
alert("Resource is unavailable");
}
else {
alert("Unexpected response status: " + xmlHttp.status);
}
}
Reading Response Headers
页面上第一个链接展示了XMLHttpRequest对象的getAllResponseHeaders()方法。这个方法只是将所有响应首部获取为一个串。在这个例子中,响应首部显示在警告框中。getAllResponseHeaders()方法的用途很有限,因为它把所有响应首部放在一起作为串返回。要想使用getAllResponseHeaders()方法来获取单个的响应首部,就需要解析返回的串,查找所关注的响应首部。
getResponseHeader方法可以通过只返回一个响应首部的值解决这个问题。这个方法取一个串参数,该参数表示所需响应首部的名字(就是你想得到这个响应首部的值)。这个例子使用getResponseHeader方法将Last-Modified首部显示在警告框中。在实际应用中,getResponseHeader方法很可能用于以某个间隔轮询服务器资源。只有自最近一次轮询服务器资源以来Last-Modified响应首部发生变化时,浏览器才会根据服务器资源更新其内容。
页面上的后两个链接使用了XMLHttpRequest对象的另一种能力,即检查服务器返回的HTTP状态码。XMLHttpRequest对象的status属性把HTTP状态作为一个整数返回。如果状态码为200,指示这是正常的成功服务器响应。相反,如果状态码是500,则指示服务器处理请求时出现了某种内部错误。
这个例子使用HTTP状态码来确定服务器资源是否可用。HTTP状态码404指示没有所请求的资源。页面上的Read Available Resource(读取可用资源)链接请求位于服务器上的简单的XML文件。因为服务器上有这个文件,所以HTTP 状态码为200,指示这是成功的响应。页面上最后一个链接是Read Unavailabl
e Resource(读取不可用资源),它请求服务器上没有的文件。服务器会用HTT P状态码404做出响应。JavaScript事件处理程序检查服务器响应,看到404状态码,显示警告框指示所请求的资源不可用。
代码清单4-4显示了readingResponseHeaders.xml。
代码清单4-4readingResponseHeaders.xml
显示所有响应首部的结果如图4-3所示。图4-4显示了读取Last-Modified首部的结果,图4-5显示了确定Web资源是否可用的结果。
图4-3显示所有响应首部
图4-4读取一个响应首部,在这里是Last-Modified首部
图4-5确定Web资源是否可用
4.3 动态加载列表框
Web应用通常使用“向导工具”设计原则来构建,即每个屏幕要求用户输入少量的信息,每个后续页的数据都依据前一页的输入来创建。对于某些情况,这个设计模式非常有用,如用户以一种逐步、有序的方式完成任务。遗憾的是,太多的Web应用使用了这种方法,因为它们别无选择。在Ajax技术出现之前,当基于用户输入修改页面上的某些部分时,动态地更新页面而不刷新整个页面是很难办到的,甚至根本不可能。
避免完全页面刷新的一种技术是在页面上隐藏数据,并在需要时再显示它们。例如,假设选择框B的值要根据选择框A中所选值来填写,此时选择框B的所有可取值就可以放在隐藏的选择框中。当选择框A中的所选值有变化时,JavaScr ipt可以确定要显示哪一个隐藏的选择框,然后将该选择框置为可见,再把前一个选择框置为隐藏。这种技术还可以变化一下,用隐藏列表框中的元素动态填写选择框B中的option元素。这些技术都很有用,但是它们只在有限的情况下可用,即页面中仅限于根据用户输入对有限的选择进行修改,而且这样的选择必须相对少。
假设你在构建一个在线的汽车分类广告服务。某人想购买汽车,指定了车型年份、品牌和车型,来搜索他想买的汽车。为了避免用户的输入错误,并减少所需的动态验证次数,你决定车型年份、品牌和车型输入字段都应当是选择框,而
且要考虑过去25年的车型广告。如果车型年份选择框或品牌选择框中的选择发生变化,就必须修改对应该车型年份和品牌的可用车型列表。
要记住,对于每个车型年份,都会出现一些新的品牌,而一些老牌子可能会淡出人们的视线,所以其个数也会有变化。还要记住对于每种品牌来说,每年的车型都可能不同。如果有数十种品牌,每个车型年份每种品牌都有多种车型,那么车型年份、品牌和车型的组合数将是惊人的。由于有这么多的组合,只使用J avaScript来填写选择框是不可能的。
使用Ajax技术就能很轻松地解决这个问题。车型年份或品牌选择框中的选择每次有变化时,会向服务器发出异步请求,要求得到该车型年份特定品牌的车型列表。服务器负责根据浏览器所请求的品牌和车型年份来确定车型列表。服务器很可能采用一种高速的数据查找组件(可能实现为一个关系数据库),以完成查找可用车型的具体工作。一旦找到可用的车型,服务器把它们打包在一个XML 文件中,并返回给浏览器。
浏览器负责解析服务器的XML响应,并用指定品牌和车型年份的可用车型来填写车型选择框。在这个例子中,要注意数据视图与原始数据得到了很好的分离。浏览器只负责呈现数据视图,服务器则负责挖掘必须呈现在浏览器视图上的原始数据。
代码清单4-5展示了如何使用Ajax技术,从而根据另外两个列表框的值动态创建一个选择框的内容。这个例子的用例就是以上所述的分类广告服务,在此车型年份选择框和品牌选择框中的所选值决定了车型选择框中的内容。这个例子中只用了4个车型年份、3种品牌,以及对于某个车型年份、特定的品牌的4种可用车型。即便如此,车型年份、品牌和车型的组合数也达到了48。如果采用隐藏的办法,即对应每个车型年份和品牌组件,将相应的车型列表隐藏起来,并根据所选的品牌和车型年份值来显示适当的列表,这是不可行的。
代码清单4-5dynamicLists.html
"https://www.360docs.net/doc/2219284619.html,/TR/xhtml1/DTD/xhtml1-strict.dtd">
var xmlHttp;
function createXMLHttpRequest() {
if (window.ActiveXObject) {
xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
}
else if (window.XMLHttpRequest) {
xmlHttp = new XMLHttpRequest();
}
}
function refreshModelList() {
var make = document.getElementById("make").value;
var modelYear = document.getElementById("modelYear").value;
if(make == "" || modelYear == "") {
clearModelsList();
return;
}
var url = "RefreshModelList?"
+ createQueryString(make, modelYear) + "&ts=" + new Date().getTime();
createXMLHttpRequest();
xmlHttp.onreadystatechange = handleStateChange;
xmlHttp.open("GET", url, true);
xmlHttp.send(null);
}
function createQueryString(make, modelYear) {
var queryString = "make=" + make + "&modelYear=" + modelYear;
return queryString;
}
function handleStateChange() {
if(xmlHttp.readyState == 4) {
if(xmlHttp.status == 200) {
updateModelsList();
}
}
}
function updateModelsList() {
clearModelsList();
var models = document.getElementById("models");
var results = xmlHttp.responseXML.getElementsByTagName("model");
var option = null;
for(var i = 0; i < results.length; i++) {
option = document.createElement("option");
option.appendChild
(document.createTextNode(results[i].firstChild.nodeValue));
models.appendChild(option);
}
}
function clearModelsList() {
var models = document.getElementById("models");
while(models.childNodes.length > 0) {
models.removeChild(models.childNodes[0]);
}
}
Select Model Year and Make
页面的更新由品牌和车型年份选择框的onchange事件驱动。只要这两个选择框中任何一个的所选值有变化,浏览器就会向服务器发出异步请求。发送请求时会携带一个查询串,其中包含所选品牌和车型年份的值。
RefreshModelList servlet从浏览器接收到请求,并确定对应指定品牌和车型年份的车型列表。这个servlet首先解析查询串,确定所请求的品牌和车型年份。一旦确定了品牌和车型年份,servlet会迭代处理一个对象集合,其中每个对象分别表示一种车型年份、品牌和车型的组合。如果特定对象的车型年份和品牌属
性与所请求的车型年份和品牌匹配,则把这个对象的车型属性增加到响应XML
串中。找到对应指定品牌和车型年份的所有车型之后,将响应XML写回到浏览器。
请注意,在实际实现中,服务器端组件不太可能依赖硬编码的值填写选择框,而是会搜索一个高速数据库,查找所请求车型年份和品牌的相应车型。
代码清单4-6显示了RefreshModelListServlet.java。
代码清单4-6RefreshModelListServlet.java
package ajaxbook.chap4;
import java.io.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.servlet.*;
import javax.servlet.http.*;
public class RefreshModelListServlet extends HttpServlet {
private static List availableModels = new ArrayList();
protected void processRequest(HttpServletRequest request
, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
int modelYear = Integer.parseInt(request.getParameter("modelYear"));
String make = request.getParameter("make");
StringBuffer results = new StringBuffer("
MakeModelYear availableModel = null;
for(Iterator it = availableModels.iterator(); it.hasNext();) {
availableModel = (MakeModelYear)it.next();
if(availableModel.modelYear == modelYear) {
if(availableModel.make.equals(make)) {
results.append("
results.append(availableModel.model);
results.append("");
}
}
}
results.append("");
response.setContentType("text/xml");
response.getWriter().write(results.toString());
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
processRequest(request, response);
}
public void init() throws ServletException {
availableModels.add(new MakeModelYear(2006, "Dodge", "Charger"));
availableModels.add(new MakeModelYear(2006, "Dodge", "Magnum"));
availableModels.add(new MakeModelYear(2006, "Dodge", "Ram"));
availableModels.add(new MakeModelYear(2006, "Dodge", "Viper"));
availableModels.add(new MakeModelYear(1995, "Dodge", "Avenger")); availableModels.add(new MakeModelYear(1995, "Dodge", "Intrepid")); availableModels.add(new MakeModelYear(1995, "Dodge", "Neon")); availableModels.add(new MakeModelYear(1995, "Dodge", "Spirit")); availableModels.add(new MakeModelYear(1985, "Dodge", "Aries")); availableModels.add(new MakeModelYear(1985, "Dodge", "Daytona")); availableModels.add(new MakeModelYear(1985, "Dodge", "Diplomat")); availableModels.add(new MakeModelYear(1985, "Dodge", "Omni")); availableModels.add(new MakeModelYear(1970, "Dodge", "Challenger")); availableModels.add(new MakeModelYear(1970, "Dodge", "Charger")); availableModels.add(new MakeModelYear(1970, "Dodge", "Coronet")); availableModels.add(new MakeModelYear(1970, "Dodge", "Dart"));
availableModels.add(new MakeModelYear(2006, "Chevrolet", "Colorado")); availableModels.add(new MakeModelYear(2006, "Chevrolet", "Corvette")); availableModels.add(new MakeModelYear(2006, "Chevrolet", "Equinox")); availableModels.add(new MakeModelYear(2006, "Chevrolet", "Monte Carlo")); availableModels.add(new MakeModelYear(1995, "Chevrolet", "Beretta")); availableModels.add(new MakeModelYear(1995, "Chevrolet", "Camaro")); availableModels.add(new MakeModelYear(1995, "Chevrolet", "Cavalier")); availableModels.add(new MakeModelYear(1995, "Chevrolet", "Lumina")); availableModels.add(new MakeModelYear(1985, "Chevrolet", "Cavalier")); availableModels.add(new MakeModelYear(1985, "Chevrolet", "Chevette")); availableModels.add(new MakeModelYear(1985, "Chevrolet", "Celebrity")); availableModels.add(new MakeModelYear(1985, "Chevrolet", "Citation II")); availableModels.add(new MakeModelYear(1970, "Chevrolet", "Bel Air")); availableModels.add(new MakeModelYear(1970, "Chevrolet", "Caprice")); availableModels.add(new MakeModelYear(1970, "Chevrolet", "Chevelle"));
availableModels.add(new MakeModelYear(1970, "Chevrolet", "Monte Carlo"));
availableModels.add(new MakeModelYear(2006, "Pontiac", "G6"));
availableModels.add(new MakeModelYear(2006, "Pontiac", "Grand Prix"));
availableModels.add(new MakeModelYear(2006, "Pontiac", "Solstice"));
availableModels.add(new MakeModelYear(2006, "Pontiac", "Vibe"));
availableModels.add(new MakeModelYear(1995, "Pontiac", "Bonneville"));
availableModels.add(new MakeModelYear(1995, "Pontiac", "Grand Am"));
availableModels.add(new MakeModelYear(1995, "Pontiac", "Grand Prix"));
availableModels.add(new MakeModelYear(1995, "Pontiac", "Firebird"));
availableModels.add(new MakeModelYear(1985, "Pontiac", "6000"));
availableModels.add(new MakeModelYear(1985, "Pontiac", "Fiero"));
availableModels.add(new MakeModelYear(1985, "Pontiac", "Grand Prix"));
availableModels.add(new MakeModelYear(1985, "Pontiac", "Parisienne"));
availableModels.add(new MakeModelYear(1970, "Pontiac", "Catalina"));
availableModels.add(new MakeModelYear(1970, "Pontiac", "GTO"));
availableModels.add(new MakeModelYear(1970, "Pontiac", "LeMans"));
availableModels.add(new MakeModelYear(1970, "Pontiac", "Tempest"));
}
private static class MakeModelYear {
private int modelYear;
private String make;
private String model;
public MakeModelYear(int modelYear, String make, String model) { this.modelYear = modelYear;
this.make = make;