原文: http://www.abigdreamer.com/mywork/webqq-update-online-reverse-ajax-implementation-off-the-assembly-line-simulation.html
本blog已轉移到造夢師http://www.abigdreamer.com,歡迎你們常去個人blog看看!
說明:本次更新只是模擬了一下人員的上下線,並無採用真是的數據(我一臺電腦開那麼多瀏覽器測試的話有點受不了,誰叫電腦爛呢,呵呵) 。另外須要注意的就是,須要用這個新的dwr.jar覆蓋掉原來的那個dwr.jar,要不會出問題的!
有朋友問我怎樣用反向AJAX來實如今線人員列表的更新,提到DWR容許javascript訪問服務器端的Java方法,這使得AJAX使用起來會比較容易,而在DWR2.0裏面添加了一個很是強大的功能——反向AJAX,也就是說,服務器端的Java方法能夠取得瀏覽器端的Web上下文,並能夠調用javascript的方法,將服務器端的數據異步地傳輸給瀏覽器。本文將經過一個demo展現這種特性。這個demo實現了相似股票交易系統中實時更新數據的功能,事實上是經過發佈-訂閱模式去實現的。也就是說,客戶端訂閱一個主題,服務器端經過一個線程向訂閱這個主題的瀏覽器定時、異步地發送數據,從而實現了這種實時更新的功能。
咱們知道,客戶端瀏覽器能夠隨時鏈接到web服務器,並向服務器請求資源,而服務器卻沒有這種能力,它不能主動地於客戶端瀏覽器創建鏈接,並主動地將數據發送給瀏覽器。DWR支持3種從服務器端發送數據給客戶端的方式:
一、輪詢。客戶端在每一個時間週期內向服務器發送請求,看看服務器端有沒有數據更新,若是有,就向服務器請求數據。
二、Comet:基於HTTP長鏈接的服務器推進方式。客戶端向服務器發送請求後,服務器將數據經過response發送給客戶端,但並不會將此response關閉,而是一直經過response將最新的數據發送給客戶端瀏覽器,直到客戶端瀏覽器關閉。
三、PiggyBack(回傳)。服務器端將最新的數據排成隊列,而後等待客戶端下一次請求,接收到請求後就將等待更新的數據發給客戶端。
這3種方式各有優劣,而DWR能夠同時支持輪詢和Comet。
首先,咱們要讓DWR程序支持反向AJAX。只須要在web.xml中添加以下配置:
javascript
<servlet> <servlet-name>dwr-invoker</servlet-name> <servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class> <init-param> <param-name>debug</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>activeReverseAjaxEnabled</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>initApplicationScopeCreatorsAtStartup</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>maxWaitAfterWrite</param-name> <param-value>100</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dwr-invoker</servlet-name> <url-pattern>/dwr/*</url-pattern> </servlet-mapping>
將初始化參數activeReverseAjaxEnabled設置爲true表示啓動反向AJAX。另外,添加這個功能的核心之處在於服務器端的發佈者——Publisher.java,在這個類裏面,首先經過org.directwebremoting.WebContext來得到訪問這個應用的Web上下文:
html
WebContext webContext = WebContextFactory.get(); ServletContext servletContext = webContext.getServletContext(); serverContext = ServerContextFactory.get(servletContext); webContext.getScriptSessionsByPage("");
這裏主要經過WebContext類得到DWR應用的WEB上下文,用ServletContext得到DWRServlet的上下文,以及經過WEB上下文獲取訪問本應用的客戶端瀏覽器的ScriptSession。經過ScriptSession,能夠在服務器端向客戶端瀏覽器發出執行js方法的指令,並把服務器端數據傳送給js方法,具體的用法以下:
java
Collection sessions = serverContext .getScriptSessionsByPage("/webQQ/webQQ.jsp"); ScriptProxy proxy = new ScriptProxy(sessions); WebQQUser user = users.getNextUserStatu(); proxy.addFunctionCall("publishUserStatus", user);
這段代碼首先經過getScriptSessionsByPage方法得到全部訪問/webQQ/webQQ.jsp這個資源的客戶端瀏覽器的ScriptSession,併爲這些session創建代理(ScriptProxy),經過這個代理,讓客戶端執行publishUserStatus的js方法。其中addFunctionCall就是向客戶端發送執行js方法的服務器端方法,第一個參數是js方法的簽名,後面的都是js方法的參數。user是要發佈的即時數據。user這個對象是隨機生成的(見org.darkness.test.webqq. WebQQUsers類),Publish.java這個類啓動了一個線程(worker),這個線程不斷地生成user的數據,併發布給客戶端。
如下是html頁面的核心部分的代碼:
node
var listeners = []; function subscribeUserStatus(callBackHandler){ listeners.push(callBackHandler); } function publishUserStatus(user){ for(var i=0;i<listeners.length;i++){ listeners[i].fun.call(listeners[i].scope,user); } } subscribeUserStatus({ fun:function(user){ try{ var rootChildNodes = Ext.getCmp('im-tree').root.childNodes; for(var i=0;i<rootChildNodes.length;i++){ var childNodes = rootChildNodes[i].childNodes; for(var j=0;j<childNodes.length;j++){ if(childNodes[j].id == user.account){ if(user.isShowOnline == 0){ childNodes[j].getUI().getIconEl().src = 'images/user_delete.gif'; var tempChild = childNodes[j]; // appendChild就會將其放置到最尾端,不須要再remove了 //rootChildNodes[i].removeChild(tempChild); rootChildNodes[i].appendChild(tempChild); }else{ childNodes[j].getUI().getIconEl().src = 'images/user.gif'; // 將剛上線的人員移動到最頂端 var firstChild = rootChildNodes[i].firstChild; var tempChild = childNodes[j]; rootChildNodes[i].insertBefore(tempChild, firstChild); } // 原本想用TreeNode自帶的排序方法的,可試過了,老是沒反應 //rootChildNodes[i].sort(function(nodeA, nodeB){ // return nodeA.getUI().getIconEl().src > nodeB.getUI().getIconEl().src; //}, rootChildNodes[i]); return; } } } }catch(e){ } }, scope:this }); dwr.engine.setActiveReverseAjax(true);
這一塊代碼主要是將改變在線或下線的用戶的圖標。
subscribeUserStatus方法訂閱在線人員列表的數據(這些數據由服務器端的Java方法進行發佈)。其中經過一個匿名回調函數,將取得改變的數據在頁面顯示出來(即人員的上、下線)。該回調方法很是簡單,只是將TreeNode組件中的圖標改變了一下,實現了實時提醒用戶上下線的功能。
另外,服務器端還有一個監聽器PublisherServletContextListener,這是爲了在適當的時候關閉發佈者的線程。這個監聽器要結合其餘兩個DWR的監聽器使用,只需在web.xml裏面聲明就好了:
web
<listener> <listener-class>org.directwebremoting.servlet.EfficientShutdownServletContextAttributeListener</listener-class> </listener> <listener> <listener-class>org.directwebremoting.servlet.EfficientShutdownServletContextListener</listener-class> </listener> <listener> <listener-class> org.darkness.webqq.web.servlet.PublisherServletContextListener</listener-class> </listener>
最後,看一下dwr的映射關係dwr.xml:
ajax
<dwr> <allow> <create creator="new" javascript="Publisher" scope="application"> <param name="class" value="org.darkness.webqq.web.servlet.Publisher"/> </create> <convert converter="bean" match="org.darkness.webqq.model.WebQQUser"/> <!-- this is a bad idea for live, but can be useful in testing --> <convert converter="exception" match="java.lang.Exception"/> <convert converter="bean" match="java.lang.StackTraceElement"/> </allow> </dwr>
注意<convert converter="bean" match="org.darkness.webqq.model.WebQQUser"/>這個配置,dwr容許將自定義的Java類型與js對象進行相互轉換,但要聲明轉換器。
如下是程序運行的結果:
瀏覽器