四、 基于 Dojo 的 pub-sub 事件模型
pub-sub 是 publish-subscribe 的缩写,意思是 发布-订阅, 它提供了一种事件处理函数的注册方式。实际上, Struts 2 的 pub-sub 事件模型基本是采用了 Dojo 的 pub-sub 事件模型
pub-sub 事件模型提供了一种简化的事件监听方式,通过 pub-sub 事件模型,可以让一个 JavaScript 事件同时触发多个事件处理函数。当我们把一个事件(也可能是一个普通函数) 作为一个发布者注册到一个主题后,如果该事件被触发(普通函数被调用),则该主题下所有的事件处理函数都会被自动调用。如下图:
图 pub-sub 事件模型示意图
Struts 2 并未对 Dojo 的 pub-sub 事件模型进行过多的包装,如果有 Dojo 事件模型的基础,就可以非常容易理解 Struts 2 的 pub-sub 事件模型。
在 Dojo 中将一个事件注册到某个主题下的代码如下:
//将 foo 对象的 bar 方法注册到 /refresh 主题 dojo.event.topic.publish("/refresh","foo","bar");
提示: 事件主题是一个任意字符串,并没有太多额外的要求。
将某个事件处理函数注册到某个主题下的代码如下:
// 在 /refresh 主题下增加了一个匿名事件处理函数 dojo.event.topic.subscribe("/refresh", function(param1,param2){ //this function will be called everytime "/refresh" is published });
当然,Struts 2 在 Dojo 的 pub-sub 事件模型基础之上,进行了简单的包装,它为大部分 Ajax 标签提供了如下 2 个属性:
● listenTopics : 指定系列事件主题名,多个主题之间以英文逗号(,)隔开。配置该元素的 HTML 元素将用于加载服务器响应。
● notifyTopics : 指定系列事件主题名, 多个主题之间以英文逗号(,)隔开。配置该属性的 HTML 元素将会把事件发布到指定主题,发布主题时会传递 3 个参数: data、type、request
因为 Struts 2 标签发布的事件总是会发送 3 个参数: data 、type 、request,这3个参数就代表了与服务器交互的内容
3 个参数的含义如下:
● data : 从服务器返回的数据
● type : 与服务器交互的状态,只有 3 个值: before (交互之前)、 load (加载数据中) 、 error (服务器响应出错)
● request : 代表请求对象本身
因此, Struts 2 中的事件处理函数大致上都是如下形式:
//事件处理函数包含 3 个参数 function a(data,type,request){ }
下面代码中使用了一个按钮来发布事件,将事件发布到指定主题;并通过 JavaScript 代码来为该主题增加一个事件处理函数。
/pub-sub/pub-sub1.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>pub-sub模型</title> <sx:head/> <script type="text/javascript"> //订阅/lee主题,使用匿名函数监听 dojo.event.topic.subscribe("/lee", function(data, type, request) { alert('正处于Dojo的异步交互过程中,类型是:' + type); alert(request); }); </script> </head> <body> <h3>简单pub-sub模型</h3> <!-- 单击该按钮时将发布事件到/lee主题 --> <sx:submit type="submit" value="提交" align="left" notifyTopics="/lee"/> </body> </html>
上面代码中,定义 “提交“ 按钮时 ,指定了 notifyTopics="/lee" 属性,该属性指定单击按钮时,单击事件将被发布到 /lee 主题
在上面 JavaScript 代码中,使用 dojo.event.topic.subscribe("/lee", function(data, type, request){}); 代码为 /lee 主题注册了一个匿名的事件处理函数。
上面事例代码是直接指定了事件处理函数作为 /lee 主题的订阅者。除此之外,我们也可以指定一个或多个 Struts 2 标签元素作为主题的订阅者,定义订阅者通过为 Struts 2 标签指定 listenTopics 属性实现。一旦指定了一个或多个 Struts 2 标签作为订阅者,这样每当有事件被发布到该主题时,这些 Struts 2 标签将被 “激发” 一次
/pub-sub/pub-sub2.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>pub-sub模型</title> <sx:head/> </head> <body> <h3>pub-sub模型</h3> <!-- 定义发布事件的提交按钮 --> <sx:submit type="submit" value="更新" notifyTopics="/change"/> <!-- 定义2个订阅事件的Struts 2标签 --> <sx:div id="t1" cssStyle="background-color:#bbbbbb;width:360px; height:80px" listenTopics="/change" href="../ajax/book.action"/> <sx:div id="t2" cssStyle="border:2px solid black;width:360px; height:120px" listenTopics="/change" href="../ajax/price.action" /> </body> </html>
上面代码让一个 “更新 ” 按钮作为事件发布者 ,让两个 <sx:div /> 元素作为事件的订阅者 ,意思就是每当单击 “更新 ” 按钮时,该按钮将会把单击事件发布到 /change 主题,而订阅者 <sx:div /> 元素的默认行为(更新自己的内容)被触发 。关于 <sx:div /> 标签的用法,请看下一节。
使用 <sx:div /> 元素时,指定了一个 href 属性,该属性指向一个 Action , 该 Action 负责生成该元素里的内容。上面 2 个 book.action 和 price.action 都直接使用 ActionSupport 作为处理类。
struts.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.1//EN" "http://struts.apache.org/dtds/struts-2.1.dtd"> <struts> <constant name="struts1custom.i18n.resources" value="messageResource"/> <constant name="struts.i18n.encoding" value="GBK"/> <package name="ajax" extends="struts-default" namespace="/ajax"> <action name="*"> <result>/pub-sub/{1}.jsp</result> </action> <action name=""> <result>.</result> </action> </package> </struts>
/pub-sub/book.jsp
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %> <%=Math.random() > 0.5 ? "疯狂Java讲义" : "Struts 2权威指南"%>
/pub-sub/price.jsp
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %> <h3>今日报价</h3> <ul> <li>西红柿:<%=java.lang.Math.random() * 10%>元/斤</li> <li>黄瓜:<%=java.lang.Math.random() * 10%>元/斤</li> <li>芹菜:<%=java.lang.Math.random() * 10%>元/斤</li> </ul>
通过上面可以看出,每次单击“更新 ”按钮时,将会引起页面中 2 个 DIV 元素里内容的改变。
从上面的介绍可以看出,Struts 2 的 pub-sub 事件模型基本上保留了 Dojo 事件模型特征,而且在原有的基础上,提供了进一步简化。通过使用 Struts 2 的事件模型,可以以更简单的方式来动态更新页面的内容。
<script type="text/javascript"> function newWin(url){ window.open(url, "newwindow", "width=800,height=500,top="+(screen.availHeight-500)/2+",left="+(screen.availWidth-600)/2+", toolbar=no, menubar=no, scrollbars=yes, resizable=no, location=yes, status=no"); } </script> <s:a href="" onclick="newWin('pub-sub/pub-sub1.jsp');" cssStyle="cursor: hand;">pub-sub1.jsp</s:a> <s:a href="" onclick="newWin('pub-sub/pub-sub2.jsp');" cssStyle="cursor: hand;">pub-sub2.jsp</s:a>
如果想阻止某个 Ajax 过程,则可以先将该 Ajax 标签的事件注册到某个事件主题下,再为该主题增加事件订阅者,并在该事件订阅者中取消该请求
例如,可以通过如下代码将 Ajax 按钮的 Ajax 事件注册到 /request 事件主题下:
<sx:submit type="submit" value="submit" notifyTopics="/request" href="ajaxText.action"/>
下面为 /request 事件主题增加订阅者,并在订阅者中取消 request 如下:
//为 /request 事件主题增加订阅者 dojo.event.topic.subscribe("/request",function(data,type,request){ //取消 request 的 Ajax 请求 request.cancel= true; });
通过上面代码即可强行中止 Ajax 请求
Struts 2 的 Dojo 插件为了简化 Ajax 过程,提供了一些常用的 Ajax 标签,这些常用的 Ajax 标签基本可以满足普通的 Ajax 需要。但对于一些更复杂的 Ajax 通信过程,Struts 2 提供的 Ajax 标签则不够灵活,所以可以使用后面的 JSON 插件来实现
<sx:div /> 标签在页面上生成一个 HTML 的 <div/> 元素,但这个 <div/> 元素的内容不是静态内容,而是从服务器获取的内容。为了让 <sx:div/> 标签能取得服务器的数据,必须为 <sx:div /> 标签指定一个 href 属性,这个 href 属性值是一个 Action ,该 Action 负责生成该 <div/> 元素的内容 。
还可指定该 <sx:div/> 标签生成的 <div/> 元素以固定的频率来更新自身的内容,为了指定更新频率和更新延迟,可以指定如下两个属性:
● updateFreq : 指定更新 Div 内容的时间间隔,单位是 ms 。如果不指定,则只在页面加载时更新该 Div 的内容
● delay : 指定更新 Div 内容的时间延迟,单位 ms , 如果不指定,且指定了 updateFreq 属性,则页面加载后立即开始计时,准备刷新该 Div 的内容。如果没有指定 updateFreq ,则该属性没有任何意义
如果服务器的响应包含了 JavaScript 代码,且希望在本页面内执行,则可以为该 div 标签指定 executeScripts = "true"
/dojo/sx-div1.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>远程div</title> <sx:head/> </head> <body> 仅一次获取服务器内容的div<br /> <sx:div id="div1" cssStyle="border: 1px solid black;background-color:#dddddd; width:300px;height:40px;padding-top:8px;padding-left:20px" href="random.action"> 初始化文本 </sx:div> 动态更新内容的div,每隔1s刷新一次(通过指定updateFreq="1000")<br /> 使用indicator(通过指定indicator="indicator")<br /> <sx:div id="div2" cssStyle="border: 1px solid black;background-color:#dddddd; width:300px;height:40px;padding-top:8px;padding-left:20px" href="random.action" updateFreq="1000" indicator="indicator"> 初始化文本 </sx:div> <img id="indicator" alt="Loading..." style="display:none" src="${pageContext.request.contextPath}/dojo/images/indicator.gif"/><br /> 3s之后才开始更新(通过指定delay="3000")<br /> 指定与服务器交互出错的提示(通过指定errorText属性)<br /> 指定与服务器交互过程中的提示(通过指定loadText属性)<br /> <sx:div id="div3" cssStyle="border: 1px solid black;background-color:#dddddd; width:300px;height:40px;padding-top:8px;padding-left:20px" href="random.action" updateFreq="1000" delay="3000" errorText="加载服务器数据出错" loadingText="正在加载服务器内容"> 初始化文本 </sx:div> 指定显示系统出错提示(通过指定showErrorTransportText="true")<br> <sx:div id="div4" cssStyle="border: 1px solid black;background-color:#dddddd; width:300px;height:40px;padding-top:8px;padding-left:20px" href="/dojo/AjaxNoUrl.jsp" updateFreq="1000" showErrorTransportText="true" loadingText="正在加载服务器内容"> 初始化文本 </sx:div> 执行服务器脚本(通过指定executeScripts="true") <sx:div id="div5" cssStyle="border: 1px solid black;background-color:#dddddd; width:300px;height:40px;padding-top:8px;padding-left:20px" href="test.action" updateFreq="9000" executeScripts="true" loadingText="正在加载服务器内容"> 初始化文本 </sx:div> </body> </html>
上面的页面代码中,分别定义了几种不同的 div ,有的没有 updateFreq 属性,则该 div 的内容只在页面加载时一次更新;有的指定了 updateFreq ,则指定该 div 内容的固定更新频率;有的指定了 executeScripts="true" ,则该 div 元素可以执行远程服务器端的 JavaScript 代码。
除此之外,上面的 Div 还使用了如下 2 个 Ajax 标签的通用属性:
● showErrorTransportText : 设置是否显示服务器的错误信息
● loadingText : 设置当服务器响应还未加载时的显示信息
/dojo/randomStr.jsp
<%@ page contentType="text/html;charset=GBK" language="java" %> <%@ 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="rdmStr"/>
/dojo/testjs.jsp
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %> <% 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); %> <script language="JavaScript" type="text/javascript"> alert('疯狂Java讲义'); </script> 轻量级Java EE企业应用实战 <script language="JavaScript" type="text/javascript"> alert('疯狂Ajax讲义'); </script>
上面包含了一些 JavaScript 代码,为了在目标页面中执行这些 JavaScript 代码,需要为 <sx:div > 标签指定 executeScripts="true" 属性
RandomAction
package js.dojo; import com.opensymphony.xwork2.ActionSupport; public class RandomAction extends ActionSupport { private String data; public String getRdmStr() { String result = Math.round(Math.random() * 10000) + ""; // 返回data和随机字符串连缀而成的字符串 return data != null && !data.equals("") ? data + result : result; } public void setData(String data) { this.data = HTMLDecoder.decode(data); } public String getData() { return this.data; } }
HTMLDecoder
package js.dojo; 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(); } }
Struts.xml
<struts> <constant name="struts.custom.i18n.resources" value="messageResource"/> <constant name="struts.i18n.encoding" value="GBK"/> <include file="struts_4.xml"/> <package name="js" extends="struts-default" namespace="/09">
<?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.dojo" extends="struts-default"> <action name="random" class="js.dojo.RandomAction"> <result>/dojo/randomStr.jsp</result> </action> <!-- 配置ajaxText Action --> <action name="ajaxTest" class="js.dojo.AjaxTestAction"> <result>/dojo/ajaxResult.jsp</result> </action> <action name="test"> <result>/dojo/testjs.jsp</result> </action> <action name="books"> <result>/dojo/data_zh_CN</result> </action> <action name="getBooks" class="js.dojo.GetBooksAction"> <result>/dojo/books.jsp</result> </action> <action name="showPerson" class="js.dojo.ShowPersonAction"> <result>/dojo/showPerson.jsp</result> </action> <action name="getChild" class="js.dojo.GetChildNode"> <result>/dojo/bookNode.jsp</result> </action> <action name=""> <result>.</result> </action> </package> </struts>
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="" onclick="newWin('dojo/sx-div1.jsp');" cssStyle="cursor: hand;">sx-div1.jsp</s:a>
如果不需要 div 调用远程 java 方法,而是定期执行某个 JavaScript ,则可以为该 div 标签指定一个 handler 属性,该属性的值为该 JavaScript 函数即可,如下:
/dojo/sx-div2.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>远程div</title> <sx:head/> </head> <script type="text/javascript"> function handler(widget, node) { alert('本地JavaScript函数处理动态div'); node.innerHTML = Math.random() > 0.4 ? "疯狂Java讲义" : "Struts 2权威指南"; } </script> <body> 直接使用本页面的JS函数,不再调用远程服务器<br /> <sx:div id="div1" cssStyle="border: 1px solid black;background-color:#dddddd; width:300px;height:40px;padding-top:8px;padding-left:20px" href="random.action" updateFreq="4000" handler="handler"> 初始化文本 </sx:div> </body> </html>
上面代码中,为 <sx:div> 指定了一个 handler 属性,该属性指向一个 JavaScript 函数。这意味着 div 每次刷新,该 JavaScript 函数都会被调用。 上面即指定了 handler 又指定了 href ,但该 div 标签不会再调用 href 属性指定的 Action ,所以 href 属性可以去除
除此之外,<sx:div> 标签还可将一个表单里包含的所有表单域转换成请求参数,并且把这些请求参数发送给远程服务器 。为了让一个 <sx:div> 标签发送表单里包含的表单域,可以为该 <sx:div> 标签指定如下属性:
● formId : 该属性的属性值为一个表单元素的 ID , 表明该 <sx:div> 标签会把该表单里的所有表单域转换成请求参数发送到服务器
● formFilter : 指定一个函数用于过滤指定表单内的表单域
除此之外,为了通过在 JavaScript 代码中手动控制 <sx:div> 标签启动自动更新,关闭自动更新,则可以为该 div 标签指定如下 2 个属性:
● startTimerListenTopics : 设置一个监听的事件主题,当有 Struts 2 组件向该主题发布事件时,该 div 标签的计时器被启动
● stopTimerListenTopics : 设置一个监听的事件主题,当有 Struts 2 组件向该主题发布事件时,该 div 标签的计时器被关闭
/dojo/sx-div3.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>远程div</title> <sx:head/> </head> <script type="text/javascript"> var controller = { refresh : function() {alert("手动刷新");}, start : function() {alert("启动自动刷新");}, stop : function() {alert("停止自动刷新");} }; //将controller的refresh方法注册成/refresh主题的发布者 dojo.event.topic.registerPublisher("/refresh", controller, "refresh"); //将controller的start方法注册成/startTimer主题的发布者 dojo.event.topic.registerPublisher("/startTimer", controller, "start"); //将controller的stop方法注册成/stopTimer主题的发布者 dojo.event.topic.registerPublisher("/stopTimer" , controller, "stop"); //为after主题指定一个事件订阅者 dojo.event.topic.subscribe("/after" , function(data, type, e){ alert('与服务器交互过程中. 现在的过程类型是:' + type); }); </script> <body> <form id="testForm"> <s:textfield name="data" label="输入用户数据"/> </form> <input type="button" value="手动刷新" onclick="controller.refresh();" /> <input type="button" value="停止计时器" onclick="controller.stop();" /> <input type="button" value="启动计时器" onclick="controller.start();" /> <br/> 使用pub-sub机制(通过指定listenTopics等属性)<br/> 发送表单请求参数(通过指定formId="form")<br/> <sx:div id="div1" cssStyle="border: 1px solid black;background-color:#dddddd; width:300px;height:40px;padding-top:8px;padding-left:20px" href="random.action" loadingText="正在加载服务器内容..." listenTopics="/refresh" startTimerListenTopics="/startTimer" stopTimerListenTopics="/stopTimer" updateFreq="9000" autoStart="true" formId="testForm" notifyTopics="/after"> 初始化文本 </sx:div> </body> </html>
从上面代码中可以看出,单击“停止计时器 ” ,触发 controller 的 stop 方法,该方法会发布一个事件到 /stopTimer 主题,而页面中的 <sx:div > 标签的 stopTimeListenTopics 属性正是 /stopTimer ,这将引起计时器停止计时,即该<sx:div> 标签的内容停止自动更新。
点击“手动刷新”或则 “启动计时器”,当在文本框中输入 java 字符串,将在下面的文本框内看到 java 字符串,即表单里的请求参数已经被发送到了服务器