用Dynamic Faces构建Ajax聊天室
作者:Yuanxin Li Yuan-Xin.Li@sun.com projectair@163.com
本文将介绍如何使用Dynamic Faces在Netbeans 6.1中基于Visual Web JSF构建一个简单的Ajax聊天室。Dynamic Faces是JSF的一个扩展组件库,使用该组建库可以使得JSF容易地实现AJAX交互.由于Dynamic Faces还没兼容到Java EE 5标准中,因此,本例使用的是J2EE 1.4的代码级别,而且必须使用Visual Web JSF的UI组件库(注意,不是最新的webui组件库)。
软件需求:
1、JDK 1.6以上
2、Netbeans 6.1,并安装Visual Web JSF Backward Capability Kit,Project Dynamic Faces Ajax Componets插件.
3、Glassfish或Sun Application Server
实验步骤:
1、建立工程:点击文件-新建项目-Web-Web应用程序,下一步,将项目命名为AjaxChat,点击下一步,注意,Java EE版本必须选择J2EE 1.4,而且"将源代码级别设置为1.4"前面必须选中,如下图所示:
点击下一步,选择Visual Web JSF框架:
点击完成。
2、配置web.xml,打开WEB-INF下的web.xml,点击Servlet选项,在初始化参数栏点击"添加按钮",参数名称为javax.faces.LIFECYCLE_ID,参数值为com.sun.faces.lifecycle.PARTIAL,确认后保存所有修改.这里的配置是把DynamicFaces注入到JSF的生命周期中去.如下图所示:
3、添加组建库:展开项目,右键单击"库",选择添加库,选择Dynamic Faces Components(0.2)。双击打开Page1,在组件面板空白部分点击右键,选择添加组建库,将Dynamic Faces Components(0.2)添加到组件面板中,添加成功后,组件面板中就会出现Dynamic Faces两个组件Ajax Zone 和 Ajax Trasaction,如下图所示:
4、在ApplicationBean中添加代码。打开源包中的ApplicationBean1,将以下代码添加到该类中,并修复导入:
ApplicationBean1.java private int nextAnonIndex; private final int MAX_ENTRIES = 100; //list of String arrays, with each array of length 2 private List entryList = new LinkedList(); //array containing same contents as entryList private String[][] entries; public String[][] getEntries() { synchronized(this.entryList) { return this.entries; } } public void addEntry(String username, String comment) { if (comment == null || comment.length() < 1) { return; } if (username == null) { username = "anonymous"; } synchronized(this.entryList) { if (this.entryList.size() == MAX_ENTRIES) { this.entryList.remove(0); } String[] entry = new String[]{username, comment}; this.entryList.add(entry); this.entries = (String[][])this.entryList.toArray( new String[this.entryList.size()][entry.length]); } } public synchronized String getNextAnonUsername() { nextAnonIndex++; return "anonymous" + nextAnonIndex; } 这段代码的目的是读写所有的用户资料和聊天记录。这些记录是用一个二维数组来存储的,第一维用于存储用户名,第二维用来存储该用户对应的聊天记录,通过synchronized关键字来进行同步读写控制。5、在SessionBean1中添加以下代码: SessionBean1.java private String username; public synchronized String getUsername() { return username; } public synchronized void setUsername(String username) { this.username = username; } 这段代码的目的是在每个用户的会话中保存和读取其用户名。当用户提交聊天记录时,需要读取其用户名并写入ApplicationBean中。 6、在Page1.java中prerender方法(页面渲染前执行)添加以下代码: Page1.java prerender() String username = (String) getExternalContext().getRequestParameterMap().get("username"); if (username != null) { getSessionBean1().setUsername(username); } else if (getSessionBean1().getUsername() == null) { getSessionBean1().setUsername(getApplicationBean1().getNextAnonUsername()); } 这段代码在页面渲染前执行,首先获取URL中的username参数,并设置到用户会话中。如果是匿名用户,则通过ApplicationBean来获取匿名用户ID。 在Page1.java的末尾添加以下代码:(最后一个括号前) Page1.java public String getTranscript() { String[][] entries = getApplicationBean1().getEntries(); if (entries == null) { return null; } StringBuffer sb = new StringBuffer(); for (int i = 0; i < entries.length; i++) { String entryUsername = entries[i][0]; String comment = entries[i][1]; String color = "purple"; String username = getSessionBean1().getUsername(); if (username.equals(entryUsername)) { color = "blue"; } sb.append( "
["); sb.append(entryUsername); sb.append("] "); sb.append(comment); sb.append("
"); } return sb.toString(); } 这段代码的目的是读取所有的聊天记录并将其格式化,为下一步渲染到页面做好准备。 7、设计页面:首先,打开Page1的设计窗口,从组件面板的“布局”中拖一两个个布局面板到页面中,将大小大概调整为如下:点击上面那个组件面板,在属性窗口中,将id改为transcriptPanel。打开jsp窗口,进入jsp代码,找到
这样设置的目的是当布局面板中的内容过长时,会显示一个滚动条。点击保存全部,回到设计窗口,从布局面板的“基本”中拖一个静态文本到上面那个布局面板里面,注意拖进去的时候,布局面板的边框会变成蓝色。点击静态文本,在属性窗口中把id设为transcriptText,回到设计窗口,并去掉"escape"前面的勾(保留html标签)点击静态文本的右键,选择“绑定到数据”-“绑定到对象”,然后选择transcript,这样,静态文本的内容就被绑定到getTranscript()方法中,也就是所有的聊天记录了。如下图所示:接下来,从组件面板"基本"中拖一个文本框和按钮到下面那个布局面板里面,把文本框的id属性设为comment,把columns属性设为50.把按钮的id属性改为send,Text属性改为Send,大致效果如下:如果你使用的是Netbeans6.1,接下来要做的事情是为文本框添加绑定。这个非常有意思,因为以前的Visual Web版本中,页面的每一个组件都会在后台的托管Java Bean中初始化,这样,使得服务器端的代码非常庞大,而且页面组件多时,渲染的时候会很慢。在新的Visual Web里面,往设计窗口拖组件时,后台并不会自动生成托管对象,而是让开发者自己选择需要托管的组件。在这里,我们在后台只需要对文本框进行操作,因此,我们只需要托管文本框这个组件。右键单击文本框,选择"添加绑定属性"并保存全部。这样,在后台的Managed Component Definition里面就会自动生成以下代码了: private TextField comment = new TextField(); public TextField getComment() { return comment; } public void setComment(TextField tf) { this.comment = tf; }双击按钮,进入事件处理方法的编辑,加入以下代码: Page1.java send_action() public String send_action() { // TODO:处理按钮单击操作。返回的值是一个导航 // 条件名称,如果它为 null,则返回到同一页。 String comment = (String)getComment().getText(); String username = getSessionBean1().getUsername(); getApplicationBean1().addEntry(username, comment); return null; } 这段代码的目的是把用户每次发送的消息写入到ApplicationBean中,这样,其他用户就可以读取的到了。8、配置Ajax Transaction。在这一步,我们把所有的与服务器的交互以Ajax的方式来实现。(1)点击设计窗口的工具栏上的按钮,目的是把虚拟表单的内容显示出来(包含Ajax Transaction).(2)从组件面板的"Dynamic Faces"中把Ajax Transaction拖到页面中,在页面的右下角会出现以下图标:(3)按住ctrl键选中文本框和按钮,点击右键,选择Configure Ajax Transactions,在弹出的窗口顶部,出现comment和send,表明现在在为这两个组件配置Ajax交互。把Name改为commentTx,Send Input改为Yes。点击确认完成。在设计窗口中,我们看到文本框和按钮的边框变为蓝色。这一步的设置使得由comment和send产生的输入在commentTx被激活时通过Ajax request来发送请求。(4)在设计窗口中选中静态文本,点击右键,选择Configure Ajax Transactions,在弹出窗口的顶部显示的是TranscriptText. 点击窗口中的"New"按钮,把新的Transaction的Color改为红色,Name改为pollTx,两个Re-render都设为Yes,如下图所示:点击ok后,设计窗口中的静态文本的外围出现了蓝色和红色的边框,如下图所示:这一步的设置使得静态文本在接收到由commentTx or pollTx的Ajax Transactions产生的Ajax response的时候都会重新渲染。(5)为Transaction添加脚本方法签名。这一步添加的脚本方法签名的实现会在后续步骤中描述。在Page1-导航窗口中展开Page1 > page1 > html1 > head1,选择commentTx Ajax Transaction。在属性窗口中,把postReplace属性设为customPostReplaceForCommentTx,这里表明在客户端接收到commentTx的Ajax response后,通过customPostReplaceForCommentTx重新渲染相应的组件。(6)选择pollTx,在属性窗口中,保持inputs和execute为空,这里表明当pollTx被激活时,所有的输入并不会添加到Ajax response中。把postReplace属性设为customPostReplaceForPollTx,replaceElement属性设为customReplaceForPollTx。replaceElement属性的方法会在组件重新渲染后执行。 9、为页面设置javascript属性。在导航窗口,展开Page1 > page1 > html1,选择body1,在属性窗口中,把onLoad属性设为handleOnLoad(),把onUnload属性设为handleOnUnload()。选择form1组件,在属性窗口中,把onSubmit属性设为return interceptFormSubmit()。这个方法屏蔽了传统的表格提交方式,而激活 commentTx Ajax Transaction。10、 添加javascript。在项目窗口,点击resource节点的右键,新建-其他-其他-Javascript文件,命名为ajaxchatroom。在ajaxchatroom.js中添加以下代码: ajaxchatroom.js var pollDelay = 1000; var continuePolling = false; var mouseDownOnTranscript = false; //whether the user has performed a mousedown on the transcript (including any scrollbar) //and not yet performed a mouseupfunction customPostReplaceForCommentTx(element, markup) { //scroll to the bottom of the transcript var transcriptPanel = document.getElementById('form1:transcriptPanel'); transcriptPanel.scrollTop = transcriptPanel.scrollHeight; //clear the text field var commentTextField = document.getElementById('form1:comment'); commentTextField.value = ''; //place focus in the text field commentTextField.focus(); }function handleOnLoad() { //handle mousedown on the transcript var transcriptPanel = document.getElementById('form1:transcriptPanel'); transcriptPanel.onmousedown = handleMouseDown; //handle mouseup anywhere on the page document.onmouseup = handleMouseUp; //turn autocomplete off for the text field document.getElementById('form1:comment').setAttribute('autocomplete','off'); //start polling continuePolling = true; poll(); }function handleOnUnload() { //stop polling continuePolling = false; }function poll() { //fire the pollTx Ajax Transaction DynaFaces.Tx.fire('pollTx'); }function customReplaceForPollTx(element, markup) { //provided that the user is not performing an operation such as selecting transcript text or scrolling the transcript, //perform replacement (re-rendering) for this poll request, //and scroll the transcript as appropriate after the replacement if (!mouseDownOnTranscript) { var transcriptPanel = document.getElementById('form1:transcriptPanel'); //scrollTop: distance between top of transcript and top of the portion currently visible //scrollHeight: total height of transcript, including any portion not visible due to scrolling //clientHeight: height of the visible portion of the transcript //capture whether scrollbar exists before replacement. //scrollbar exists if the scrollHeight exceeds the clientHeight var scrollbarExistsBeforeReplacement = transcriptPanel.scrollHeight > transcriptPanel.clientHeight; //capture whether the transcript is scrolled to the bottom before replacement var scrolledToBottomBeforeReplacement = false; if (scrollbarExistsBeforeReplacement) { //transcript is scrolled to the bottom if the sum of scrollTop and clientHeight equals scrollHeight if (transcriptPanel.scrollTop + transcriptPanel.clientHeight == transcriptPanel.scrollHeight) { scrolledToBottomBeforeReplacement = true; } } //capture the scrollTop before replacement var scrollTopBeforeReplacement = transcriptPanel.scrollTop; //invoke default replacement function to perform actual replacement of transcript content DynaFaces.replace(element, markup); //capture whether scrollbar exists after replacement var scrollbarExistsAfterReplacement = transcriptPanel.scrollHeight > transcriptPanel.clientHeight; //scroll to the bottom of the transcript if it was scrolled to the bottom before replacement //or if the scrollbar did not exist before replacement and it now exists after replacement. //otherwise, scroll the transcript to the same place it was before replacement if (scrolledToBottomBeforeReplacement || (!scrollbarExistsBeforeReplacement && scrollbarExistsAfterReplacement)) { transcriptPanel.scrollTop = transcriptPanel.scrollHeight; //scroll to the bottom of the transcript } else { transcriptPanel.scrollTop = scrollTopBeforeReplacement; //scroll transcript to the place it was before replacement } }}function handleMouseDown() { //if the mousedown occurs on the transcript's scrollbar, //IE will not invoke the corresponding mouseup event handler when the mouse is released. //in such a case, do not set mouseDownOnTranscript to true, since nothing will set it back to false. //instead, just perform replacement in customReplaceForPollTx even though the user is scrolling the transcript, //as this does not seem to cause a problem on IE anyway if (document.all) { var transcriptPanel = document.getElementById('form1:transcriptPanel'); var scrollBarExists = transcriptPanel.scrollHeight > transcriptPanel.clientHeight; if (scrollBarExists) { if (window.event.offsetX > transcriptPanel.clientWidth) { //mousedown occurred on the scrollbar return; } } } mouseDownOnTranscript = true; }function handleMouseUp() { mouseDownOnTranscript = false; }function customPostReplaceForPollTx(element, markup) { //send the next poll request if (continuePolling) { setTimeout(poll, pollDelay); }}function interceptFormSubmit() { //if the text field is not blank, fire commentTx if (document.getElementById('form1:comment').value != '') { DynaFaces.Tx.fire('commentTx'); } //prevent conventional form submission return false; } 在这段javascript代码里面,最主要的语句是DynaFaces.Tx.fire('...'); 语句,这个语句在进行适当的设置(主要是滚动条的设置)和一些变量的设置后激活Ajax Transaction,从而进行Ajax交互。例如在interceptFormSubmit方法中,如果获取文本框的值不为空时,就激活commentTx,把输入通过Ajaxrequest传给服务器端进行处理,当Ajax返回response的时候,又会执行customPostReplaceForPollTx方法,在1秒钟的延时后执行poll()方法。poll方法通过DynaFaces.Tx.fire('pollTx'); 语句激活pollTx,进而执行customReplaceForPollTx方法进行静态文本和布局面板的重新渲染。由于静态文本的Text属性是绑定到后台的getTranscript()方法的,因此页面会局部刷新服务器取到的值,完成Ajax交互。 11、添加script组件。在组件面板的"高级"列表里拖一个"脚本"组件到设计窗口中,在属性窗口的URL的属性中,选择ajaxchatroom.js。12、生成、部署和运行项目,也可以在URL后面指定用户名,在多个浏览器里面模拟聊天的场景,下图是运行后的一个结果:参考资料:http://www.netbeans.org/kb/60/web/ajaxchatroom.html
用Dynamic Faces构建Ajax聊天室
Groovy高效编程——使用Mock & Stub简化测试
精通 Grails:Grails 服务和 Google 地图
高效率创建安全的 Java 应用,第 2 部分
OAuth不断获得动力
WebKit小组宣布开发基于字节码的JavaScript解释器——SquirrelFish