搞定J2EE核心技术与企业应用
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

4.5 XMLHttpRequest概述

XMLHttpRequest是Ajax开发必备的对象,虽然Iframe也能实现它的部分功能,但它提供了客户端和服务器端异步通信的能力。相对于HttpRequest来说,XMLHttpRequest不仅能实现HttpRequest的功能,而且还不需要重新加载页面。

4.5.1 XMLHttpRequest的生命周期

XMLHttpRequest的生命周期主要包括:初始化、发送请求、设置回调函数、处理返回值4个部分,下面一一进行讲解。

XMLHttpRequest的初始化即XMLHttpRequest的创建工作,目前没有统一的标准,在不同的浏览器中实现存在着不同的方法。

在IE 5.0和IE 6.0的相关版本中,XMLHttpRequest对象的实现方式如下:

      var xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");

而在其他的浏览器中,XMLHttpRequest对象的实现方式如下:

      var xmlhttp=new XMLHttpRequest();

为了使其实现方式一致,在IE 7.0版本以后,XMLHttpRequest对象的实现方式也改为如下方式:

      var xmlhttp = new XMLHttpRequest();

为了提高兼容性,所以在创建一个XMLHttpRequest对象时,一般采用如下写法:

      var xmlhttp;
      if (window.XMLHttpRequest) {
          //其他浏览器
          xmlhttp = new XMLHttpRequest();
      } else if (window.ActiveXObject) {
          try {
              //IE浏览器
              xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
          } catch (e) {
              xmlhttp = new ActiveXObject('Msxml2.XMLHTTP');
          }
      }

这种写法类似于JSON-RPC的写法,JSON-RPC中创建XMLHttpRequest对象的示例代码如下:

      /*将各种可能的XMLHttpRequest对象放在数组中*/
      JSONRpcClient.msxmlNames = [ "MSXML2.XMLHTTP.5.0",
                      "MSXML2.XMLHTTP.4.0",
                      "MSXML2.XMLHTTP.3.0",
                      "MSXML2.XMLHTTP",
                          "Microsoft.XMLHTTP" ];
      JSONRpcClient.getHTTPRequest =
      function JSONRpcClient_getHTTPRequest()
      {
          /*Mozilla浏览器的创建 */
          try {
          JSONRpcClient.httpObjectName = "XMLHttpRequest";
          return new XMLHttpRequest();
          } catch(e) {}
          /*微软IE浏览器的创建*/
          for (var i=0;i < JSONRpcClient.msxmlNames.length; i++) {
          try {
              //一个一个循环创建,直到有一个成功
              JSONRpcClient.httpObjectName = JSONRpcClient.msxmlNames[i];
              return new ActiveXObject(JSONRpcClient.msxmlNames[i]);
          } catch (e) {}
          }
          /* 如果没有创建成功*/
          JSONRpcClient.httpObjectName = null;
          throw new JSONRpcClient.Exception(0, "Can't create XMLHttpRequest object");
      };

DWR也提供了类似的写法,DWR中创建XMLHttpRequest对象的示例代码如下:

      /*将各种可能的XMLHttpRequest对象放在数组中*/
      dwr.engine._XMLHTTP = ["Msxml2.XMLHTTP.6.0", "Msxml2.XMLHTTP.5.0", "Msxml2.XMLHTTP.4.0",
      "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP"];
      /*假如为非IE浏览器*/
      if (window.XMLHttpRequest) {
              batch.req = new XMLHttpRequest();
          }
          /*假如为IE浏览器*/
          else if (window.ActiveXObject && !(navigator.userAgent.indexOf("Mac") >= 0 && navigator.
          userAgent.indexOf("MSIE") >= 0)) {
              batch.req = dwr.engine._newActiveXObject(dwr.engine._XMLHTTP);
          }
      dwr.engine._newActiveXObject = function(axarray) {
          var returnValue;
          /*一个一个循环创建,直到有一个成功*/
          for (var i = 0; i < axarray.length; i++) {
              try {
                  returnValue = new ActiveXObject(axarray[i]);
                  break;
              }
              catch (ex) { /* ignore */ }
          }
          return returnValue;
      };

针对上述问题,Prototype给出了一个很好的解决方案,在Prototype中定义了一个Try.these()函数,示例代码如下:

      var Try = {
          these: function() {
              var returnValue;
              //遍历参数,分别创建
              for (var i = 0, length = arguments.length; i < length; i++) {
              var lambda = arguments[i];
              try {
                  //如果创建成功,则中止
                  returnValue = lambda();
                  break;
              } catch (e) {}
              }
              return returnValue;
          }
      }

通过这个函数,前面创建XMLHttpRequest对象的方式就可以改为如下所示:

      var Ajax.xmlhttp = Try.these(
              //分别创建,直到有一个成功
              function() {return new XMLHttpRequest()},
              function() {return new ActiveXObject('Msxml2.XMLHTTP')},
              function() {return new ActiveXObject('Microsoft.XMLHTTP')}
      ) || false;//如果都不成功,则返回false

由此可以看出Prototype设计的巧妙,读者可以根据Prototype的设计思路进行类推,从而为自己以后的JavaScript设计提供帮助。

通过上面给出的JSON-RPC、DWR和Prototype创建XMLHttpRequest对象的不同方式可以比较出它们之间的特点,也可以看出它们支持浏览器的类型和版本。

XMLHttpRequest对象创建完毕后,即可开始请求连接服务器端,XMLHttpRequest可以采用同步或异步方式与服务器端通信。同步方式适用于数据量非常少的场合;一般情况下,在Ajax中都使用异步方式来与服务器端通信。通过XMLHttpRequest对象的open方法的第3个参数,可以用来设定是采用同步还是异步方式,参数为true代表异步方式,为false代表同步方式,示例代码如下:

      xmlhttp.open("GET", "http://localhost:8080/index.jsp", true);

XMLHttpRequest对象的open方法共有以下5个参数。

● request-type:发送请求的类型,如GET、POST或HEAD。

● url:要连接的URL。

● asynch:如果希望使用异步方式连接则为true,否则为false。该参数是可选的,默认值为true。

● username:如果需要身份验证,则可以在此指定用户名。该可选参数没有默认值。

● password:如果需要身份验证,则可以在此指定口令。该可选参数没有默认值。

设定通信的相关参数,即调用open方法,并不代表已经把信息发送给服务器端了,只代表这个请求已经建立,要想把信息发送给服务器端,还要调用send方法。调用send方法的示例代码如下:

      xmlhttp.send(body);

send方法有一个参数body,表示要发送的信息,其格式和Web方式下发送信息的格式一样,如下所示:

      var body = ''username=gd&password=gf'';

如果采用GET的通信方式,则服务器端通信可以采用request.querystring()的方法获取值;如果采用POST的通信方式,则服务器端通信可以采用request.form()的方法来获取值,当然也可以不传值,即body=null。另外,如果采用POST的通信方式传送信息给服务器端,则必须先调用setRequestHeader方法,修改MIME类型。

在Prototype中的实现代码如下:

      setRequestHeaders: function() {
          var headers = {//设置各种不同的头参数
              'X-Requested-With': 'XMLHttpRequest',
              'X-Prototype-Version': Prototype.Version,
              'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
          };
          //判断通信方式是否设定为POST
          if (this.method == 'post') {
              headers['Content-type'] = this.options.contentType +
                  (this.options.encoding ? '; charset=' + this.options.encoding : '');
              /*单独处理比较老版本的Mozilla浏览器,把'Connection'关闭*/
              if (this.transport.overrideMimeType &&
                  (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
                      headers['Connection'] = 'close';
          }
          //如果用户自己定义了头参数
          if (typeof this.options.requestHeaders == 'object') {
              var extras = this.options.requestHeaders;
              if (typeof extras.push == 'function')
              for (var i = 0, length = extras.length; i < length; i += 2)
                  headers[extras[i]] = extras[i+1];
              else
                  $H(extras).each(function(pair) { headers[pair.key] = pair.value });
          }
          //如果用户定义了多个头参数
          for (var name in headers)
              this.transport.setRequestHeader(name, headers[name]);
          }

如果前面设定为采用异步方式来进行通信,则执行完send方法后,会立即执行接下来的语句,因为采用异步方式要设定回调函数,等状态改变时可以调用回调函数;但是如果设定为采用同步方式来进行通信,则执行完send方法后,会一直等待直到服务器端处理完毕,并返回数值后或者请求超时后才会接着执行下面的语句。

如果是采用同步方式来和服务器端通信,那么在服务器端返回数据后,再调用函数进行处理就可以了;但是如果是采用异步方式来和服务器端通信,客户端不知道服务器端会在何时返回数据,也就没有办法知道该在何时调用函数进行操作了。为了解决这个问题,XMLHttpRequest对象实现了一个onreadystatechange事件,它根据XMLHttpRequest对象的状态属性readyState的改变来触发函数,onreadystatechange事件在每一次readyState的状态值改变时都会被触发。

设置回调函数的示例代码如下:

      //设定回调函数
      xmlhttp.onreadystatechange=function(){
          //获取返回值
          var msgWeclome = xmlhttp.responseText;
          var msg = document.getElementById("msg");
          msg.innerHTML = msgWeclome;
      }

一般情况下,只有readyState的状态值为4时才进行实际的函数处理,这是因为状态值为4代表请求完毕,数据已接收成功,此时进行处理才有意义。readyState的状态值共有5个,如表4.23所示。

表4.23 readyState的状态值

就算readyState的状态值是4,也只是表示请求的状态,代表了服务器端数据已接收成功,并不表示请求的结果是否成功,即请求是否已经成功返回客户端。为解决这个问题, XMLHttpRequest对象又实现了一个属性status,用来表示请求响应代码。status的状态值有很多,这里只列出几个常用的,如表4.24所示。

表4.24 status的常用状态值

有了这两个属性,在设置回调函数时,一般都要增加对这两个属性状态的判断,示例代码如下:

      //设定回调函数
      xmlhttp.onreadystatechange=function(){
          if (4 == xmlhttp.readystate) {
              if (200 == xmlhttp.status) {
                  //获取返回值
                  var msgWeclome = xmlhttp.responseText;
                  var msg = document.getElementById("msg");
                  msg.innerHTML = msgWeclome;
              }
          }
      }

XMLHttpRequest对象一般常用的处理返回值的属性有两个,分别是responseXML和responseText。

(1)responseXML,顾名思义,采用这个属性来处理返回值,返回的肯定是XML格式的文档对象,而且是已经解析好的文档对象,可以直接操作。这里要说明的一点是,如果请求的直接是XML对象,则在服务器端不需要设定Response的ContentType值,服务器会自动设定;如果请求的结果是动态生成的XML对象,则在服务器端要设定Response的ContentType值,设定方法如下:

      response.setContentType("text/xml;charset=UTF-8");

注意:加上charset=UTF-8的意思是防止中文乱码。

(2)responseText,采用这个属性来处理返回值,返回的是纯文本代码,未经过加工处理。

4.5.2 XMLHttpRequest的方法和属性

在4.5.1节的“XMLHttpRequest的生命周期”中,已经讲解了它的一部分方法和属性, XMLHttpRequest还提供了其他的一些方法和属性,下面一一进行讲解。

XMLHttpRequest的常用方法如表4.25所示。

表4.25 XMLHttpRequest的常用方法

XMLHttpRequest的常用属性如表4.26所示。

表4.26 XMLHttpRequest的常用属性

4.5.3 建立XMLHttpRequest对象池

因为使用XMLHttpRequest对象具有异步通信的功能,所以在一个页面中原来页面刷新一次只能进行一次和服务器交互的功能,现在改为用户可以随时点击多次和服务器进行交互,这样一来就会出现问题,就是如果整个页面通用一个XMLHttpRequest对象,则只有最后一次调用的请求能有结果返回,因为新的请求会覆盖旧的请求。如果采用和服务器每交互一次就新建一个XMLHttpRequest对象的方式,则会很耗费客户端的内存,造成资源的浪费。要解决这个问题,可以采用和数据库连接池类似的方法,建立一个XMLHttpRequest对象池。

实现XMLHttpRequest对象池的大体思路是:建立一个缓存数组用来存放已经创建好的XMLHttpRequest对象,遇到用户请求需要创建XMLHttpRequest对象时,则先查看缓存数组中是否已经有XMLHttpRequest对象空闲,如果有则直接从缓存中取一个使用;如果没有则重新创建一个。

实现XMLHttpRequest对象池的示例代码如下:

      var Try = {
          these: function() {
      var returnValue;
              for (var i = 0, length = arguments.length; i < length; i++) {
                  var lambda = arguments[i];
                  try {
                      //如果创建成功,则中止
                      returnValue = lambda();
                      break;
                  } catch (e) {}
              }
              return returnValue;
          }
      };
      var Ajax = {
          getTransport: function() {
              //创建一个XMLHttp,直到成功为止
              return Try.these(
                  function() {return new XMLHttpRequest()},
                  function() {return new ActiveXObject('Msxml2.XMLHTTP')},
                  function() {return new ActiveXObject('Microsoft.XMLHTTP')}
              ) || false;
          }
      };
      var XMLHttp = {
              _xmlhttpCache: [],
              _getXmlhttp: function ()
              {
                  //判断是否有空闲的XMLHttp
                  for (var i = 0; i < this._xmlhttpCache.length; i ++)
                  {
                      if (this._xmlhttpCache[i].readyState == 0 || this._xmlhttpCache[i].readyState == 4
                      {
                          return this._xmlhttpCache[i];
                      }
                  }
                  //创建一个新的XMLHttp
                  this._xmlhttpCache[this._xmlhttpCache.length] = Ajax.getTransport();
                  //返回缓存中的一个XMLHttp
                  return this._xmlhttpCache[this._xmlhttpCache.length - 1];
              },
              send: function(method, url, data, callback){
                  var xmlhttp = this._getXmlhttp();
                  with(xmlhttp)
                      {
                          try
                          {
                              if (url.indexOf("?")!=-1) {//防止缓存
                                  url+="&requestTime="+(new Date()).getTime();
                              } else {
                                  url+="?requestTime="+(new Date()).getTime();
                              }
                      //采用异步方式调用
                      open(method, url, true);
                      //设定请求编码方式
                      setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset= UTF-8');
                      send(data);
                      onreadystatechange = function ()
                      {
                        if (xmlhttp.readyState == 4 && (xmlhttp.status == 200 || xmlhttp.status == 304))
                      {
                            //调用回调函数
                            callback(xmlhttp);
                      }
                      }
                  }
              }
          }
      };

将上述代码单独保存为gd.js文件,然后需要用到它的页面将其引入即可,引入代码示例如下:

      <script type='text/javascript' src='gd.js'></script>

只在网页中保留回调函数的代码即可实现和第8章中示例代码同样的功能,从而实现了代码的重用;当然也可以将回调函数单独保存成JS文件,从而实现HTML和JavaScript代码的分离。修改后的myHelloWorld.jsp的代码如下所示:

      //******* myHelloWorld.jsp **************
      <%@ page contentType="text/html; charset=UTF-8" language="java" import="java.sql.*" errorPage="" %>
      <html>
      <head>
      <title>Ajax</title>
      <script type='text/javascript' src='gd.js'></script>
      <script type="text/javascript">
      function ok() {
          XMLHttp.send("get", "http://localhost:8080/myHelloWorld/myHelloWorld.do",null,gf);
          //设定回调函数
          function gf(xmlhttp){
              var msgWeclome = xmlhttp.responseText;
              var msg = document.getElementById("msg");
              msg.innerHTML = msgWeclome;
          }
      }
      </script>
      </head>
      <body>
      <span id='msg'></span><br>
      <input type="button" onclick="ok()" value="单击此按钮"/>
      </body>
      </html>

4.5.4 使用Iframe代替XMLHttpRequest

在Ajax兴起之前,其实也可以实现页面无刷新即可和服务器交互的功能,那就是利用Iframe。本节讲解如何使用Iframe代替XMLHttpRequest实现Ajax的功能。

实现的大体思路是:在提交页面中嵌套一个Iframe,然后在提交时,将form的target设定为该Iframe。另外,在Servlet返回时,设定返回到一个临时页面,然后在该临时页面调用提交页面的方法来获取服务器端返回的信息,具体步骤如下:

01 用鼠标右键单击JSP文件夹,在弹出的快捷菜单中选择“New>JSP”命令,弹出“Create a new JSP page”对话框,如图4.16所示。

图4.16 “Create a new JSP page”对话框

02 输入文件名为“myHelloWorldIframe.jsp”,然后单击“Finish”按钮,即可新建JSP文件myHelloWorldIframe.jsp。

03 输入myHelloWorldIframe.jsp的代码如下所示:

      //*******myHelloWorldIframe.jsp**************
      <%@ page contentType="text/html; charset=UTF-8" language="java" import="java.sql.*" errorPage="" %>
      <%@ page import="java.sql.*,java.util.*,javax.servlet.*,
              javax.servlet.http.*,java.text.*,java.math.*"
      %>
      <%
      //获取服务器端传来的数据
      String msg = (String)((request.getAttribute("msg") == null) ? "" : (String)request.getAttribute ("msg"));
      %>
      <html>
      <head>
      <title>使用Iframe代替XMLHttpRequest</title>
      <script type="text/javascript">
      function ok() {
          form1.target ="sendMess";
          form1.submit();
      }
      //得到从子页面(myHelloWorldIframeSend.jsp)返回的信息显示
          function getMsg(str) {
              var msg = document.getElementById("msg");
              msg.innerHTML = str;
          }
      </script>
      </head>
      <body>
      <form name="form1" action="/myHelloWorld/myHelloWorld.do" method="get">
      <span id='msg'><%=msg%></span><br>
      <input type="button" onclick="ok();" value="单击此按钮"/>
      <iframe name="sendMess" style="display:none" ></iframe>
      </form>
      </body>
      </html>

04 使用同样的方法新建myHelloWorldIframeSend.jsp文件,输入myHelloWorldIframeSend.jsp的代码如下所示:

      //******* myHelloWorldIframeSend.jsp **************
      <%@ page contentType="text/html; charset=UTF-8" language="java" import="java.sql.*" errorPage="" %>
      <%@ page import="java.sql.*,java.util.*,javax.servlet.*,
              javax.servlet.http.*,java.text.*,java.math.*"
      %>
      <%
      //获取服务器端传来的数据
      String msg = (String)((request.getAttribute("msg") == null) ? "" : (String)request.getAttribute ("msg"));
      %>
      <html>
      <head>
      <title>使用Iframe代替XMLHttpRequest</title>
      <script type="text/javascript">
      //返回信息到父页面(myHelloWorldIframe.jsp)
      function setReturnMsg(msg) {
          window.parent.getMsg(msg);
      }
      setReturnMsg('<%=msg%>');
      </script>
      </head>
      <body>
      <form name="form1" method="post">
      </form>
      </body>
      </html>

05 编写Servlet程序HelloWorldIframe.java,主要用来获取服务器端的时间并组成问候语。用鼠标右键单击包com.myHelloWorld.web,在弹出的快捷菜单中选择“New>Servlet”命令,弹出“Create a new Servlet”对话框,如图4.17所示。在该对话框中输入相应内容,然后单击“Next”按钮就会进入关于Servlet程序配置的对话框,具体配置如图4.18所示。

图4.17 “Create a new Servlet”对话框

图4.18 配置Servlet

06 单击“Finish”按钮,即可新建类HelloWorldIframe.java。

07 输入HelloWorldIframe.java的代码如下所示:

      //******* HelloWorldIframe.java**************
      package com.myHelloWorld.web;
      import java.io.IOException;
      import java.io.PrintWriter;
      import java.text.SimpleDateFormat;
      import java.util.Date;
      //引入Servlet
      import javax.servlet.RequestDispatcher;
      import javax.servlet.ServletException;
      import javax.servlet.http.HttpServlet;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      //该类继承Servlet类
      public class HelloWorldIframe extends HttpServlet {
      public void doGet(HttpServletRequest request,HttpServletResponse response)throws IOException,
      ServletException {
              response.setContentType("text/plain;charset=UTF-8");
            request.setAttribute("msg","现在时间是:"+getCurrentDateAndTime()+" 欢迎您(HelloWorld)");
            RequestDispatcher rd = request.getRequestDispatcher("/jsp/myHelloWorldIframeSend.jsp");
              rd.forward(request, response);
          }
          /**
          * 得到当前系统日期,格式:yyyy-MM-dd HH:mm:ss
          * @return String
          */
          public String getCurrentDateAndTime() {
              String currentDate = "";
              //设定日期格式
              SimpleDateFormat format1 = new SimpleDateFormat("yyyy'-'MM'-'dd HH:mm:ss");
              format1.setLenient(false);
              currentDate = format1.format(new Date());
              //返回当前日期
              return currentDate;
          }
      }

05 查看web.xml文件,这是Web程序开发所必需的,web.xml文件的示例代码如下:

      //******* web.xml **************
      <?xml version="1.0" encoding="UTF-8"?>
      <web-app version="2.4"
          xmlns="http://java.sun.com/xml/ns/j2ee"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
          http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
      <!--设定Servlet-->
      <servlet>
              <servlet-name>Servlet</servlet-name>
              <servlet-class>com.myHelloWorld.web.HelloWorldIframe</servlet-class>
      </servlet>
      <!--设定Servlet的对应关系-->
          <servlet-mapping>
              <servlet-name>Servlet</servlet-name>
              <url-pattern>*.do</url-pattern>
          </servlet-mapping>
      </web-app>

09 上述代码完成后,在MyEclipse上启动Tomcat 7,然后在IE地址栏中输入http://localhost:8080/myHelloWorld/jsp/myHelloWorldIframe.jsp,即可看到有“单击此按钮”按钮的画面,如图4.19所示。

图4.19 有“单击此按钮”按钮的画面

10 单击“单击此按钮”按钮,即可显示服务器端的时间和问候语,如图4.20所示。

图4.20 显示出服务器端的时间和问候语