在本文中将给出一个例子来介绍使用AJAX技术从服务端获得数据的三种方法。这个例子很简单,就是两个选择框(html中的<select>标签),通过选中第一个select的某一项后,会从服务端得到一些数据,并加载到第2select中。
方法一、从服务端获得XML格式的数据
从服务端获得数据的最容易想到的方法就是在服务端反加一定格式的数据,一般是XML格式,然后在服务端使用XMLDocument或其他技术来读取这些数据,并生成<select>标签中选项的格式文本(<option>标签)。下面的addOptions函数是这个例子的核心函数,它负责根据从服务端获得的数据生成<select>标签中的<option>标签。在这里所使用的方法是利用了<select>标签的innerHTML属性(仅限于firefox),如果是IE,要使用outerHTML属性(IE<select>标签的innerHTML属性有一些小bug,读者可以试着在IE中使用innerHTML属性,看看会发生什么情况)。addOptions方法的实现代码如下:

// select表示<select>对象,xml表示XMLDocument对象
function addOptions(select, xml)
{    
    
if(select)
    {
        
var options = "";
        
for(var i = 0; i < xml.childNodes[0].childNodes.length ; i++)
        {  
            
if(xml.childNodes[0].childNodes[i].nodeName == "list")
            {
                
var s = "";
                
if(isIE())               
                    s 
= xml.childNodes[0].childNodes[i].text;         
                
else
                    s 
= xml.childNodes[0].childNodes[i].textContent
                options 
+= "<option value='" + s + "'>" ;
                options 
+= s;
                options 
+= "</option>"
            }
        }
            
        
var id = select.id;
        
if(isIE())
            select.outerHTML 
= "<SELECT id='" + id + "' onchange='onChange(this)'>" + options + "</SELECT>";
        
else
            select.innerHTML 
= options;                
        
    }
}

    onReadState函数将在XMLHttpRequest对象的异步访问服务端时调用。当readyState4时表示成功从服务端返回XML数据。这个函数的实现代码如下:

// myRequest表示XMLHttpRequest对象,selectId表示<select>标签的id属性值
function onReadyState(myRequest, selectId) 

    
if(myRequest.readyState == 4)   // 4表示成功获得相应信息
    {              
        
try
        {
            
var xml = myRequest.responseXML;   // 获得XMLDocument对象      
            var kind = document.getElementById(selectId); // 获得<select>对象
            addOptions(kind, xml);  // 向<select>标签中加入<option>标签
        }
        
catch(e)
        {
            alert(
"onReadyState:" + e);
        }
    }
}

    getData函数负责向服务端发送请求,并设置异步事件。实现代码如下:
function getData(url, selectId)
{
    
var myRequest = getXMLHTTPRequest();  // 获得一个XMLHttpRequest对象
    
    
if(myRequest)
    {
        myRequest.onreadystatechange 
=  function() // 接收获得数据状态的事件函数
        {                        
            onReadyState(myRequest, selectId);  
        }
         
        
try 
        {
            myRequest.open( 
"post", url, true);
            
        }
        
catch(e)
        {
            alert(e);
        } 
        
try
        {

            myRequest.send(
"");
  
        }
        
catch(e)
        {
            alert(e);
        }
  
    }
}

   现在本例子的核心代码已经实现完成,下一步就是在html而加载时从服务端获得第1<select>标签的数据,并将其加载到第1<select>标签中。让我们先看一下这个静态的html代码。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    
<head>
        
<title></title>
        
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        
<script type="text/javascript" src="myscript.js">
        
</script>
    
</head>
    
<body>
        
<select id="bigKind" onchange="onChange(this)" >
             
        
</select>
        
<select id="smallKind" >
           
        
</select>
    
</body>
</html>

    从上面代码可以看出,这两个<select>标签分别是bigKindsmallKind,里面并没有<option>标签,这是因为<option>标签要在javascript里动态加载。下面我们先来加载bigKind中的数据。

window.onload = onLoad
function onLoad()
{                         

    
try
    {
        getData(
"../GetXML""bigKind");
               
    }
    
catch(e)
    {
        alert(
"onLoad:" + e);
    }
}

     其中GetXML是一个Servlet程序(读者可以将其换成其他的服务端程序,如asp.netphp的)。下面是这个GetXML程序的实现代码:

package servlet;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import database.MyData;

public class GetXML extends HttpServlet
{

    
protected void proce***equest(HttpServletRequest request, HttpServletResponse response)
            
throws ServletException, IOException
    {
        response.setContentType(
"application/xml;charset=UTF-8");

        PrintWriter out 
= response.getWriter();

        
try
        {
            String s 
= request.getParameter("kind");

            out.println(
"<data>");
            
if (s == null)
            {
                
for (String key : MyData.data.keySet())
                {
                    out.println(
"<list>" + key + "</list>");
                }
            } 
else
            {
                s 
= java.net.URLDecoder.decode(s, "UTF-8");
                System.out.println(s);
                java.util.List
<String> smallKind = MyData.data.get(s);

                
if (smallKind != null)
                {
                    
for (String kind : smallKind)
                    {

                        out.println(
"<list>" + kind + "</list>");
                    }
                }
            }
            out.println(
"</data>");

        } 
finally
        {
            out.close();
        }
    }

    
protected void doGet(HttpServletRequest request, HttpServletResponse response)
            
throws ServletException, IOException
    {
        proce***equest(request, response);
    }

    
protected void doPost(HttpServletRequest request, HttpServletResponse response)
            
throws ServletException, IOException
    {
        proce***equest(request, response);
    }

    
public String getServletInfo()
    {
        
return "Short description";
    }
}

   不管读者会不会javaservlet,从这个程序中的proce***equest方法中都可以看出,首先会获得请求参数kind,如果这个参数不存在,则返回bigKind所需要的数据,以xml格式返回,类似于如下的格式:

<data>
  
<list>data1</list>
  
<list>data2</list>
</data>

    如果kind参数存在,则在MyData.data中查询第2<select>标签(smallKind)所需要的数据。data是一个Map类型。为了方便起见,本例子并未使用数据库,而是在MyData类中定义了一个静态的Map类型变量。MyData的实现代码如下:

package database;

import java.util.*;

public class MyData {

    
public static Map<String, List<String>> data;    

    
static {
        
        data 
= new HashMap<String, List<String>>();
        
        List
<String> eProducts = new LinkedList<String>();
        eProducts.add(
"手机");
        eProducts.add(
"数码/IT");
        eProducts.add(
"家电");
        eProducts.add(
"电脑");
                
        data.put(
"消费电子", eProducts);
        
        List
<String> goods = new LinkedList<String>();
        
        goods.add(
"化妆");
        goods.add(
"健康");
        goods.add(
"玩具");
        goods.add(
"办公/文体 ");
        goods.add(
"童装童鞋");
        goods.add(
"其他");
        
        data.put(
"日用百货", goods);
        
        List
<String> books = new LinkedList<String>();
        
        books.add(
"小说");
        books.add(
"动漫"); 
        books.add(
"经济");
        books.add(
"法律");
        books.add(
"计算机");
        books.add(
"英语");
        books.add(
"通讯");
        books.add(
"其他");
        
        data.put(
"图书", books)        ;                        
    }
}

    其中data变量中的key值就是bigKind中的值,而每一个key对应的值(一个List<String>对象就是smallKind中值的列表)。下面我们来实现当第1<select>标签bigKind变化时,更新smallKind标签。<select>的onchange事件函数的代码如下:

function onChange(obj)
{
    
try
    {
        getData(encodeURI(encodeURI(
"../GetXML?kind=" +obj.options[obj.selectedIndex].value)), "smallKind");
     
    }
    
catch(e)
    {
        alert(e);
    }
}
    这个函数是<select>标签的onchange事件函数。obj表示<select>标签本身。这个函数中只有一条有实际意义的语句,也就是调用了getData方法,这个方法人在onLoad方法中调用getData时差不多,只是在传送url时使用了两个encodeURI方法。由于XMLHttpRequest方法以utf-8向服务端发送数据,因此,要使用两个encodeURI向服务端发送%xx形式的utf-8编码,然后在服务端进行解析。我们在GetXML中的proce***equest方法中可以找到如下的一条语句:

= java.net.URLDecoder.decode(s, "UTF-8");

    就是进行解码操作。
    注:如果在IE中,客户端可以不使用encodeURI对带中文的URL进行编码,服务端也不用解码。在服务端仍然可以正常显示中文。但在firefox中就必须要进行编码和解码。因此,要想跨浏览器,就需要使用本文所述的方法。

方法二、直接获得<option>...</option>内容的字符串
    上面的获得数据的方法是从服务端获得了一个XML文档,并转换成XMLDocument对象,然后解析。这种方法虽然很好,但是操作XMLDocument对象还是有些麻烦,因此,我们可以在服务端直接反回<select>标签所需要的<option>标签字符串,然后将这些字符串传给<select>对象的innerHTMLouterHTML就可以了。服务端的代码和上面的实现代码类似,只需要将<data>去掉,然后将<list>改为<option>后,并使用如下的语句来设置ContentType
response.setContentType("text/html;charset=UTF-8");
客户端可通过XMLHttpRequest对象的responseText属性获得这些含有<option>的文本,并将其赋给innerHTMLouterHTML属性。这种方法虽然很方便,但并不灵活。如果客户端不使用<select>标签,而是使用<table>或其他的标签显示数据,那么返回的这些数据就没什么用处了。而即方便,又灵活的应该是下面要介绍的方法。
方法三、从服务端返回javascript代码,在客户端使用eval函数执行
    我们可以在服务端返回类似于如下的字符串:
    var options = new Array();
    options.push(‘data1’);
    options.push(‘data2’);
    然后使用eval函数执行上面的字符串,这样我们在javascript中就可以使用options数组了。我个人认为,使用数组要比使用XMLDocument更容易,代码量也更少。如果要返回更为复杂的数据,也可以使用javascript中的类或其他数据结构。根据上面的思想,新的proce***equest方法的代码如下:

    protected void proce***equest(HttpServletRequest request, HttpServletResponse response)
            
throws ServletException, IOException
    {
        response.setContentType(
"text/html;charset=UTF-8");

        PrintWriter out 
= response.getWriter();
        out.println(
"var options = new Array();");
        
try 
        {
            String s 
= request.getParameter("kind");


            
if (s == null)
            {                
                
for (String key : MyData.data.keySet())
                {
                    out.println(
"options.push('" + key + "');");
                }
            } 
else
            {
                s 
= java.net.URLDecoder.decode(s, "UTF-8");
                System.out.println(s);
                java.util.List
<String> smallKind = MyData.data.get(s);

                
if (smallKind != null)
                {
                    
for (String kind : smallKind)
                    {
                        out.println(
"options.push('" + kind + "');");
                    }
                }
            }

        } 
finally
        {
            out.close();
        }
    }

    客户端经过改进的addOptions函数如下:

// javascript表示从服务端返回的javascript代码字符串
function addOptions(select, javascript)
{    
    
if(select)
    {   
        
if(select.id == "smallKind")
        {
            
if(isIE())
                select.options.length 
= 0
        }
        
var myOptions = "";
        eval(javascript);  
//执行从服务端返回的javascript代码
        for(var i = 0; i < options.length ; i++)  // 从options数组中取数据
        {             
            
var s = "";
            
if(isIE()) 
            {
                
                select.options[select.options.length] 
= new Option(options[i], options[i]);
            }
            
else
            {
           
                myOptions 
+= "<option value='" + options[i] + "'>" ;
                myOptions 
+= options[i];
                myOptions 
+= "</option>"
            }
        }
    }
       
    
var id = select.id;
    
if(!isIE())    
        select.innerHTML 
=  myOptions;           
}

    在上面的addOptions方法中还有一个不同是在IE中使用了<select>对象的options数组来添加选择项,而不是使用outerHTML。这么做的好处是可以在onLoad方法中就获得<select>的选项值。而如果使用outerHTMLhtml未装载完时,<select>标签中选择项仍然为0。这样在onLoad方法中就无法访问<select>中的被加入项了,当然,在onchange事件中可以。 
    firefox中使用innerHTML时,在html未装载完时,只要<select>标签被装载完(也就是调用了addOptions方法后),就可以访问<select>标签中的<option>了。个人感觉这一点要从IE做得好。顺便说一句,笔者使用的是IE6,不知道ie7会是什么效果。如果哪位试过,可以跟贴。图1是本例的效果图
 
本来想提供asp.net的例子来着,结果不知怎么着,vs2008asp.net设计视图突然不响应了,谁知道是怎么回事啊??
 
《银河系列原创教程》发布
《Java Web开发速学宝典》出版,欢迎定购