一、 Ajax 概述
Ajax (Asynchronous JavaScript And XML),即异步 JavaScript 和 XML 技术,是 2006 年最风光的技术,AJAX 技术改进了传统 Web 技术;通过 Ajax 技术,浏览者与服务器之间采用异步通信机制,从而避免了浏览者的等待,带给浏览者连续的体验。
Ajax 技术是一种完全站在用户角度的技术, Ajax 技术改善了用户体验。通过 Ajax 支持,可以在 B/S(浏览器/服务器)架构上,带给用户类似 C/S(客户端/服务器)架构应用的体验
1.1 Ajax 的起源和变革
软件系统经历了 C/S 结构到 B/S 结构的变革,这种变革主要是随着 Internet 技术兴起,B/S 结构得到了大规模应用。B/S结构的应用,是对 C/S 结构的一种改进。 在这种结构下,软件应用的业务逻辑完全在应用服务器端实现,用户表现完全在 web 服务器实现,客户端只需要浏览器即可进行业务处理,是一种全新的软件系统构造技术。这种结构是当今应用软件的首选体系结构。
B/S 结构的 3 方面优势:
● B/S 结构应用的数据安全性更好,因为 B/S 结构应用的数据统一保存在服务器端,因此可以进行更有效的备份等管理。
● B/S 结构应用的数据管理更有效, B/S 结构应用无需安装区域数据服务器,所有的数据都保存在服务器端,因此所有用户都可以看到数据的实时结构。除此之外,如果应用数据库需要更新,对于 B/S 结构应用,只需要更新服务器端数据即可,可以更有效地管理
● B/S 结构应用的应用场景更加广泛,所有的客户端只要可以接入互联网,即可使用应用程序,避免了网络的限制
C/S 结构应用也有其优势,因为 C/S 结构应用的应用程序是运行在客户端的,因此 C/S 结构的应用状态是连续的(传统 B/S 结构的状态是不连续的,只能通过 HttpSession 来跟踪用户状态),而且 C/S 结构的应用直接以窗口作为载体,因此应用程序的界面更加丰富、友好。
B/S 架构的应用无需在客户端安装任何程序,客户端只需使用浏览器就可以访问应用程序,此时还需要在服务器端安装 Web 服务器。
对于 B/S结构的应用程序,应用程序状态被保存在服务器端,因此客户端使用 B/S 结构应用时,感受到的是一种不连续的体验。除此之外,因为 B/S结构应用程序是一种请求/响应的程序架构,当客户端的一个请求发送之后,在服务器的响应还未抵达客户端之前,用户将什么也不能做,只能处于等待状态,且看到的是一片空白。
传统 B/S结构的请求是一种独占式的请求。如果一个任务需要多步骤或多选项任务才能完成,在 HTML 里,一个多步骤的任务可以在单页内表达出来。但是由于 HTML 的互动性有限,便可能产生一份很长的页面,使用户感到混乱、笨拙而难以使用。或者将多个步骤分成几个页面分别提交,但传统的独占式的请求,如果前一个请求没有得到完全响应,后一个请求则不能发送。用户在等待服务器响应期间,用户的浏览器则一片空白。
传统的 Web 应用大都采用这种独占式的请求,而且每个请求对应一个页面,因此每当服务器响应到达客户端时,浏览器都会重新转载该响应,从而导致频繁的页面刷新。因为传统 B/S 结构应用的每个页面的使用时间都很短暂(只用于一次发送请求,或一次装载服务器响应),因此不可能将该页面制作成表现丰富的页面(这样客户端的下载成本太高,因此传统 B/S 结构应用的表现层页面都很简陋)
Ajax 技术的出现,完善了传统的 Web 应用的不足。 Ajax 技术使用异步方式发送用户请求: 当用户在浏览页面的同时可以发送请求,在第一个请求的服务器响应还没有完全结束时,浏览器可以再次发送请求,页面状态不会停止,即使服务器响应还没有到达,浏览者也可以浏览该页面。
当服务器响应到达客户端时,浏览器也无需重新加载整个页面,它只更新页面的部分数据,从而提高了页面的利用时间(可以使用一个页面发送无数个请求,装载无数次响应),因此可以将表现层页面制作正表现功能非常丰富的页面。
Ajax 技术的关键点在于异步发送请求。当然,因为需要让浏览器动态加载服务器响应,所以还需要利用传统的 DHTML 知识来实现 HTML 页面的动态更新。
Ajax 技术的特征除异步发送请求外,还有动态加载服务器响应数据。使用 Ajax 技术的应用能避免频繁刷新页面,服务器响应的是数据,而不是整个页面内容。 Ajax 技术负责获取服务器数据,然后将服务器数据动态加载到浏览器中。
1.2 Ajax 的核心技术
Ajax 技术的核心是 XMLHttpRequest 对象 ,该对象在 Internet Explorer 5 中首次引入。 借助于 XMLHttpRequest 对象的帮助,应用程序就可以采用异步方式发送用户请求,并处理服务器响应,避免阻塞用户动作 。
使用 Ajax 的异步模式,浏览器就不必等用户请求操作,无需重新下载整个页面,一样可以显示服务器的响应数据。
Ajax 工作过程如下 :
① JavaScript 创建 XMLHttpRequest 对象
② 使用 XMLHttpRequest 对象向服务器发送请求。发送请求时,既可以发送 GET 请求,也可以发送 POST 请求
③ 为 XMLHttpRequest 对象的 onreadystatechanged 绑定时间监听处理函数
④ 当服务器响应送回浏览器时,通过 XMLHttpRequest 对象获取服务器响应数据。
⑤ JavaScript 通过 DOM,动态更新 HTML 页面。也可以为服务器响应数据增加 CSS 样式表,在当前页面的某个部分加以显示。
1.2.1 Ajax 的核心: XMLHttpRequest
XMLHttpRequest 是整个 Ajax 技术的灵魂。 Ajax 技术的核心是异步发送请求,而 XMLHttpRequest 则是异步发送请求的对象,如果抛开异步发送请求,Ajax 的其他技术将完全失去原有的意义
最早应用 XMLHTTP 是微软的 IE5,允许在 web 页面内部使用 XMLHTTP ActiveX 组件,扩展自身功能,可以无需从当前 Web 页面发送请求,允许直接传输数据给服务器,并允许直接从服务器取数据。因此它减少了无状态连接的痛苦,还可以避免下载冗余 HTML 代码,从而提高进程的速度。
1.2.2 Ajax 的编程脚本: JavaScript 语言
JavaScript 是一种跨平台的脚本语言,是一种基于对象的脚本语言,具有简单、易用的特征,而且在绝大部分浏览器中都能运行良好
1.2.3 DOM 模型
DOM ( Document Object Model ) 是操作 HTML 和 XML 文件的一组 API,它提供了文件的结构表述。通过 DOM,可以采用编程方式操作文档结构,可以改变文档的内容。通过使用 DOM , Web 程序开发者可增加文件的节点、属性、事件,从而提供对 HTML 页面的动态更新,例如, document 就代表 “HTML 文件本身” ,table 对象则代表 HTML 的表格对象等。
DOM 的本质是 JavaScript 或 程序语言操作页面内容的一种方式。
除此之外,Ajax 技术还可以使用 XML 文件作为数据交换的格式 ,但 Ajax 并不一定需要使用 XML 作为数据交换的格式,既可以使用普通文本作为数据交换的格式,也可以使用 JSON ( JavaScript Object Notation ) 作为数据交换的格式
为了在页面生成更多丰富的表现效果,Ajax 技术也离开不了 CSS,通过 CSS 可以在 Web 页面上做出 C/S 架构的效果。
Ajax 技术将充分利用 B/S 和 C/S 的优势,形成新的 Web 开发模式。
二、 Struts 2 提供的 Ajax支持
Struts 1 完全没有任何 Ajax 支持,WebWork 则通过一些 Ajax 标签,提供了一定的支持。
Struts 2.0 到 Struts 2.1 的重要改变之一就是对 Ajax 支持的改变。
Struts 2.0 的 Ajax 支持主要以 DWR 和 Dojo 为主,并专门提供了 Ajax 主题。但由于 Dojo 版本的不稳定性,Struts 2.1 不再提供 Ajax 主题,而将原来的 Ajax 主题放入 Struts 2 的 Dojo 插件中。如果开发者还想使用 Struts 2.0 原来的 Ajax 主题,则必须手动加入 Struts 2 的 Dojo 插件
也许考虑到 Dojo 的问题,Struts 2 现在正在考虑封装 YUI (Yahoo User Interface) 作为插件,但目前,Struts 2 的 YUI 插件的最新版本是 struts2yuiplugin-01-ALPHA-6.jar ,而且没有与 Struts 2 框架一起发布,需要单独下载,所以目前为止使用 YUI 很冒险,虽然如此,Struts 的 YUI 插件已经提供 a、autocompleter、datepicker、div、head、pushbutton、submit、tab、tabview 等标签。
在目前的情况下,Struts 2 建立开发者自信使用最基本的 Prototype 、 jQuery 等 Ajax 库,而 Struts 2 则提供了类型为 stream 的 Result , 从而允许让 Action 直接生成普通的 Ajax 响应。
注意: 对于所有异步的 POST 请求,各浏览器默认都会采用 UTF -8 字符集编码。为了正确地获得请求参数,需要在服务器端调用 HttpServletRequest 的 setCharacterEncoding("UTF-8") 才能正确地解析处理请求参数。
GuessNumberAction.java
package js.ajax.a;
import com.opensymphony.xwork2.ActionSupport;
import java.io.*;
public class GuessNumberAction extends ActionSupport {
// 封装请求参数的num属性
private int num;
private String name;
// 代表服务器响应的InputStream
private InputStream inputStream;
// num属性的setter和getter方法
public void setNum(int num) {
this.num = num;
}
public int getNum() {
return this.num;
}
// name属性的setter和getter方法
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public InputStream getInputStream() {
return this.inputStream;
}
public String execute() throws Exception {
// 查看是否可以正确获取name请求参数
System.out.println("---" + getName());
long result = Math.round(Math.random() * 7);
// 如果猜的数字正确
if (result == num) {
inputStream = new StringBufferInputStream("true$" + result);
} else {
inputStream = new StringBufferInputStream("false$" + result);
}
return SUCCESS;
}
}
该Acion 提供了一个 inputStream 属性,程序将直接使用该 InputStream 作为异步请求的响应。
struts.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" "http://struts.apache.org/dtds/struts-2.2.dtd"> <struts> <constant name="struts.custom.i18n.resources" value="messageResource"/> <constant name="struts.i18n.encoding" value="UTF-8"/> <package name="js.ajax.a" extends="struts-default" namespace="/ajaxa"> <action name="guess" class="js.ajax.a.GuessNumberAction"> <!-- 直接使用二进制流作为响应 --> <result type="stream"> <param name="contentType">text/html</param> <!-- 使用getInputStream()返回值作为响应 --> <param name="inputName">inputStream</param> </result> </action> <action name=""> <result>.</result> </action> </package> </struts>
该Action 无需使用 JSP 页面作为视图资源,而是直接使用 inputStream 属性的二进制流作为响应 。
/11/ prototype-1.6.0.3.js 看附件
/11/index.jsp
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
<%
request.setCharacterEncoding("UTF-8");
String contextPath = request.getContextPath();
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>基本Ajax支持</title>
<script src="prototype-1.6.0.3.js" type="text/javascript">
</script>
<script type="text/javascript">
//处理登录函数
function guess() {
//使用Form的request发送异步请求
$("guessNum").request(
{
//指定回调函数
onComplete : function(request) {
var show = $('show');
if (request.responseText.startsWith(true)) {
show.innerHTML = "恭喜您,您猜对了!";
} else {
show.innerHTML = "不好意思,您猜错了!";
show.innerHTML += "<br />服务器数字是:"
+ request.responseText.split("$")[1];
}
}
});
}
</script>
</head>
<body>
<form id="guessNum" action="<%=contextPath %>/ajaxa/guess.action" method="post">
<table align="center">
<caption>
<h3>请猜一个0到7之间的数字:</h3>
</caption>
<tr>
<td>您的数字:<input type="text" name="num" value="0" /><br />
普通中文参数:<input type="text" name="name" /></td>
</tr>
<tr align="center">
<td><input type="button" value="猜!" οnclick="guess();" /></td>
</tr>
</table>
</form>
<div id="show">
</div>
</body>
</html>
页面使用了 Prototype 库来提交异步请求,当用户单击该按钮时,将触发 guess() 函数,而 guess 函数将借助与 Prototype 库,以异步的方式提交表单
由于设置了以异步 POST 方式来提交表单,这就意味着请求参数以 UTF-8 字符集编码;所以 struts.i18n.encoding 设置为 UTF-8
web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <display-name>struts2</display-name> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
<s:a href="" οnclick="newWin('11/index.jsp');" cssStyle="cursor: hand;">ajaxa/index.jsp</s:a>
注意: 实际上,如果开发者使用 stream 类型的 Result ,对于不包含非西欧字符的文本问题不大(GuessNumberAction 生成响应都是英文的),但一旦响应中包含了非西欧字符的文本,处理起来比较复杂,为了方便,我们完全可以为该 Action 配置 JSP 页面作为视图资源
如果开发者喜欢用 Action 直接生成 Ajax 响应,建议使用 Struts 2 的 JSON 插件,这是一个比较成熟的解决方案,后面章节将详细介绍。
考虑到 Struts 2.0 内置的 Ajax 主题是基于 Dojo 的,所以后面会介绍如何在 Struts 2.1 中使用 Dojo 插件和 Ajax 主题
三、 基于 Dojo 的异步表单
前一节的 Ajax 例子是借助于 Prototype 库来实现的,而 Struts 2 的 Dojo 插件则提供了异步表单的方式来实现 Ajax。当我们采用异步方式提交表单时,浏览器地址栏的内容也不会改变,浏览器不会重新加载新页面,浏览者依然可以在当前页面继续自己的动作,直到服务器响应到达时,Struts 2 会自动加载服务器响应。
3.1 安装 Dojo 插件
实际上,基于 Dojo 的异步表单与普通表单并没有太大的不同,关键在于提交异步表单所使用的提交按钮有所不同,它不再使用普通的提交按钮,而是使用 Dojo 插件下提供的 submit 标签来创建的提交按钮
与 Struts 2.0 不同 , Struts 2.1 已经将 Dojo 抽离成了单独的插件,而 Dojo 插件主要提供了系列 Ajax 标签。 为了在 Struts 2.1 中使用 Dojo ,必须另外安装 Dojo 插件。
安装 3 步骤:
① 将 Struts 2.1 或以上版本的 lib 目录下的 struts2-dojo-plugin-2.2.1.jar 复制到 Web 应用的 WEB-INF\lib 目录下
② 在需要使用 Dojo 标签的 JSP 页面使用如下代码导入 Dojo 标签
<%@ taglib prefix="sx" uri="/struts-dojo-tags" %>
③ 在需要使用 Dojo 标签的 JSP 页面加入 Dojo 的 head 标签。
<head>
<title>异步表单</title>
<sx:head/>
</head>
注意: <sx:head/> 标签用于为 Dojo 插件的标签提供辅助服务,它可以在页面上导入 Dojo 所需要的 CSS 库和 JavaScript 库,因此只要使用 Dojo 插件所提供的标签,就应该在该页面中使用 <sx:head/> 标签
3.2 定义异步表单
Struts 2 的异步表单与普通表单没有任何区别,区别只是异步表单不再使用 <s:submit .../> 生成提交按钮,而是使用 <sx:submit .../> 也就是 Dojo 插件的 submit 标签生成提交按钮
当使用异步表单时有一个特征: 当前页面不会被提交,服务器响应的是数据,而不是内容。因此,服务器响应的数据只是用于更新页面的局部部分
通过 2 部分控制 Ajax 表单请求响应:
① 当使用 <sx:submit/> 标签生成提交按钮时可以指定 targets 属性,该属性用指定服务器响应将更新该属性指定的多个 HTML 元素,如果需要更新多个部分,用 (, ) 隔开
② 如何更新,在默认情况下,当服务器响应到达时,默认将服务器响应输出在 targets 属性指定的 HTML 元素中,覆盖原来 HTML 元素里的内容。如果希望本页面可以执行服务器响应的 JavaScript 代码,则可以为该提交按钮指定 executeScripts="true" 属性。
例子如下 :
/remoteform/remoteform.jsp
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ taglib prefix="sx" uri="/struts-dojo-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>异步表单</title>
<sx:head/>
</head>
<body>
<div id='show' style="border:2px solid black;width:360px;height:80px">原始静态文本</div>
使用表单请求的返回值来填充另一个元素。<br />
<s:form id='theForm1' action='ajaxTest' method='post'>
<s:textfield name='data' label="请输入您喜欢的图书"/>
<sx:submit value="修改上面的静态文本" targets="show"/>
</s:form>
使用表单请求的返回值来填充本Form<br/>
<s:form id='theForm2' action='ajaxTest' method='post'>
<s:textfield name='data' label="请输入您喜欢的图书"/>
<sx:submit value="修改Form本身" targets="theForm2"/>
</s:form>
直接运行远程JavaScript(通过指定executeScripts="true")<br />
<s:form id='theForm3' action='test2' method='post'>
<s:textfield name='data' label="请输入您喜欢的图书"/>
<sx:submit value="执行远程JS" executeScripts="true" targets="show"/>
</s:form>
</body>
</html>
上面页面定义了 3 个表单 ,其中 3 个 <sx:submit /> 都指定了 targets 属性,targets 属性指定的 HTML 元素将用于展现服务器响应。最后一个 <sx:submit /> 还指定了 executeScripts="true" ,指定运行服务器生成的 JavaScript 代码
AjaxTestAction.java
package js.remoteform;
import com.opensymphony.xwork2.ActionSupport;
public class AjaxTestAction extends ActionSupport {
private static int counter = 0;
// 封装请求参数的属性
private String data;
public long getServerTime() {
return System.currentTimeMillis();
}
public int getCount() {
return ++counter;
}
// data属性的setter和getter方法
public void setData(String data) {
// 将Dojo的请求参数转换成正常字符
this.data = HTMLDecoder.decode(data);
}
public String getData() {
return this.data;
}
}
上面 Action 在处理 data 请求参数时使用了 HTMLDecoder 的 decode() 方法进行解码。这是因为: Dojo 发送请求时不是采用的普通方式进行编码,而是先对请求参数进行编码,然后在发送请求参数。
Dojo 使用 UNICODE 字符集对请求参数进行编码,例如对于“疯狂” 两个字符,Dojo 将其编码成 疯狂 形式。为了正确处理这种请求参数,提供一个类:
HTMLDecoder.java
package js.remoteform;
public class HTMLDecoder {
public static String decode(String str) {
// 获取字符串中所有数字
String[] tmp = str.split(";&#|&#|;");
StringBuffer sb = new StringBuffer("");
// 处理每个tmp数组中每个字符串元素
for (int i = 0; i < tmp.length; i++) {
// 如果该元素是5位数字,将其转换成非西欧字符
if (tmp[i].matches("\\d{5}")) {
sb.append((char) Integer.parseInt(tmp[i]));
}
// 对于普通字符
else {
sb.append(tmp[i]);
}
}
return sb.toString();
}
}
注意: HTMLDecoder 类有个小小的问题,在处理一些极端情况时将出现问题。为了更好地改进这个类,可以正则表达式搜索字符串中匹配 #&\d{5};的子串,将其转换对应的字符即可
struts.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" "http://struts.apache.org/dtds/struts-2.2.dtd"> <struts> <constant name="struts.custom.i18n.resources" value="messageResource"/> <constant name="struts.i18n.encoding" value="UTF-8"/> <package name="js.remoteform" extends="struts-default" namespace="/remoteform"> <!-- 配置ajaxTest Action,实现类为lee.AjaxTestAction --> <action name="ajaxTest" class="js.remoteform.AjaxTestAction"> <result>/remoteform/AjaxResult.jsp</result> </action> <action name="test2"> <result>/remoteform/testjs.jsp</result> </action> <action name=""> <result>.</result> </action> </package> </struts>
第二个 Action 并没有提供 Action ,而是直接使用 ActionSupport 类作为处理类
因为对于 Ajax 而言,服务器响应不再是内容,而是数据,也就是说,服务器的响应不应该是完整的页面,而应该是需要输出的数据。所以不要在服务器响应页面中使用完整的 HTML 页面标签,而是只提供需要输出的数据。如下:
/remoteform/AjaxResult.jsp
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<%
request.setAttribute("decorator", "none");
//阻止浏览器缓存页面
response.setHeader("Cache-Control","no-cache"); //HTTP 1.1
response.setHeader("Pragma","no-cache"); //HTTP 1.0
response.setDateHeader ("Expires", 0);
%>
服务器计数器: <s:property value="count"/><br />
当前时间是:<s:property value="serverTime"/><br />
服务器返回的提示是:<s:property value="data"/>
/remoteform/testjs.jsp
<%@ page contentType="text/html;charset=GBK" language="java"%>
<%
request.setAttribute("decorator", "none");
response.setHeader("Cache-Control","no-cache"); //HTTP 1.1
response.setHeader("Pragma","no-cache"); //HTTP 1.0
response.setDateHeader ("Expires", 0); //prevents caching at the proxy server
%>
Struts 2权威指南
<script type="text/javascript">
//alert("a");
var table = document.createElement("table");
var a = document.createElement("table");
//设置表格的边框为1。
a.border = 1;
var caption = a.createCaption();
a.style.backgroundColor = "red";
caption.innerHTML = "疯狂Java体系图书";
//直接取出字符串数组(实际应用可从数据库里取出数据)
books = [ '疯狂Java讲义', '轻量级Java EE企业应用实战', '疯狂Ajax讲义', '疯狂XML讲义',
'经典Java EE企业应用实战', '疯狂Workflow讲义' ];
//为表格循环插入多行
for ( var i = 0; i < books.length; i++) {
//插入行
var tr = a.insertRow(i);
//插入单元格
var td = tr.insertCell(0);
//设置单元格的内容
td.innerHTML = books[i];
}
//将表格元素添加到HTML文档内
document.body.appendChild(a);
</script>
上面页面中,包含了 JavaScript 代码,如果希望 JavaScript 代码可以在目标页面中执行,应该为提交按钮指定 executeScripts="true" 属性
JavaScript 中用 eval() 函数把 文本转换成可执行的 JavaScript
<s:a href="" οnclick="newWin('remoteform/remoteform.jsp');" cssStyle="cursor: hand;">remoteform.jsp</s:a>