UML软件工程组织

 

 

AJAX表格分页模板:探讨基于Prototype框架的javascript面向对象设计
 
2008-04-30 作者:笨笨狗 出处:javaeye.com
 

数据分页显示,是很普遍的需求,传统的实现大多是基于服务器端导航的,这种设计采用同步方式进行数据传输,用户体验很差。下面是我在学习ajax的过程中,实现的一个解决方案,不知道设计得怎么样,所以想发出来给大家参考下,恳请给予建议和指导,狗狗感激不尽!

需求概述:需要将二维数据通过表格展现给客户端,用户可以事先选择每页显示的条目数,以及数据获取方式(静态获取、异步缓存,以及异步非缓存)。三种方式简述如下——

1、静态获取方式:

一次性获取全部数据,切换页面显示时,不再发起新的异步查询,适合少量数据的分页显示。

2、异步缓存方式:

分次异步获取页面内容,并缓存访问过的页面内容,下一次访问相同页面时,直接显示缓存内容,适合量较大且内容更新频率慢的数据显示。

3、异步非缓存方式:

与第二种方式类似,只是并不缓存页面内容,每次切换新页面都发起一次异步请求,适合更新频率快的数据显示。

使用技术:

客户端——
   1、使用table定义二维数据结构,这是table最合理的使用方式;
   2、使用css控制页面展现;
   3、使用javascript发起异步查询,以及操作页面dom元素。为加快开发速度,采用成熟的Prototype框架简化复杂度。

服务器端——
   1、因为只是原型设计,决定采用Groovy脚本生成响应数据快速展示。只要遵循数据传输格式,能很快替换为别的实现。
   2、采用MySql做数据存储,模仿分页数据。

实现过程:

首先,设计静态效果页面,注意按照web标准采用合适的xhtml结构,并使用css控制其表现,页面代码如下,为简单起见,这里把css代码直接写到head头中:

xml 代码
  1. <html>  
  2.     <head>  
  3.         <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>  
  4.         <title>分页模板</title>  
  5.         <style type="text/css">  
  6.             * {  
  7.                 margin: 0;  
  8.                 padding: 0;  
  9.                 font: 12px 宋体;  
  10.             }  
  11.             body {  
  12.                 text-align: center;  
  13.             }  
  14.             #option {  
  15.                 margin: 20px auto;  
  16.             }  
  17.             #items{  
  18.                 width: 4em;  
  19.             }  
  20.             #TMPwrap {  
  21.                 text-align: center;  
  22.                 margin-top: 10px;  
  23.             }  
  24.             #pages {  
  25.                 margin: 10px auto;  
  26.                 border-collapse: collapse;  
  27.                 border: 1px #666 solid;  
  28.             }  
  29.             #pages caption {  
  30.                 width: 100%;  
  31.             }  
  32.             #pages th {  
  33.                 padding: 0.5em;  
  34.                 border-right: 1px #B9B4A1 solid;  
  35.                 background: #ECE9D8 url("images/theadbg.gif") repeat-x bottom;  
  36.                 text-align: center;               
  37.             }  
  38.             #pages td {  
  39.                 border: 1px #B9B4A1 solid;  
  40.                 border-top: none;  
  41.                 padding: 0.5em;  
  42.             }  
  43.             #navigation {  
  44.                 margin: 10px;  
  45.                 text-align: center;  
  46.             }  
  47.             #navigation a {  
  48.                 margin-right: 8px;  
  49.                 color: black;  
  50.             }  
  51.             #navigation a.active {  
  52.                 text-decoration: none;  
  53.                 color: red;  
  54.                 cursor: default;  
  55.             }  
  56.             .hidden {  
  57.                 display: none;  
  58.             }  
  59.         </style>          
  60.     </head>  
  61.     <body>  
  62.         <div id="option">  
  63.             <label for="items">每页显示条数:</label>  
  64.             <input id="items" type="text" value="4" />  
  65.             <label for="type">获取方式:</label>  
  66.             <select id="type">  
  67.                 <option value="Static">静态</option>  
  68.                 <option value="AsyncCache">异步缓存</option>  
  69.                 <option value="Async">异步非缓存</option>  
  70.             </select>  
  71.             <input id="display" type="button" value="显 示" />          
  72.         </div>  
  73.         <div id="TMPwrap">  
  74.             <table id="pages">  
  75.                 <caption>静态分页模板</caption>  
  76.                 <thead>  
  77.                 <tr>  
  78.                     <th>标题一</th>  
  79.                     <th>标题二</th>  
  80.                     <th>标题三</th>  
  81.                     <th>标题四</th>  
  82.                     <th>标题五</th>  
  83.                     <th>标题六</th>  
  84.                 </tr>  
  85.                 </thead>  
  86.                 <tbody class="">  
  87.                     <tr>  
  88.                         <td>1</td>  
  89.                         <td>1</td>  
  90.                         <td>1</td>  
  91.                         <td>1</td>  
  92.                         <td>1</td>  
  93.                         <td>1</td>  
  94.                     </tr>  
  95.                     <tr>  
  96.                         <td>2</td>  
  97.                         <td>2</td>  
  98.                         <td>2</td>  
  99.                         <td>2</td>  
  100.                         <td>2</td>  
  101.                         <td>2</td>  
  102.                     </tr>  
  103.                     <tr>  
  104.                         <td>3</td>  
  105.                         <td>3</td>  
  106.                         <td>3</td>  
  107.                         <td>3</td>  
  108.                         <td>3</td>  
  109.                         <td>3</td>  
  110.                     </tr>  
  111.                     <tr>  
  112.                         <td>4</td>  
  113.                         <td>4</td>  
  114.                         <td>4</td>  
  115.                         <td>4</td>  
  116.                         <td>4</td>  
  117.                         <td>4</td>  
  118.                     </tr>  
  119.                 </tbody>  
  120.             </table>  
  121.             <div id="navigation">  
  122.                 <a href="#" class="active">1</a>  
  123.                 <a href="#">2</a>  
  124.                 <a href="#" class="">3</a>  
  125.             </div>  
  126.         </div>  
  127.     </body>  
  128. </html>  

这样我们就制作了一个包含四行数据的表格。注意这里html标签的运用,使用thead、tbody将表格内容分成逻辑块,在后面的设计中,我们会把每一页的内容生成一个tbody。为保证兼容性,对表格的修饰,比如边框、背景、底色这些,最好在单元格(td或者th)中设定,以确保在不同的浏览器中具有相似的显示效果。

以上内容中,我们用静态页面的方式,设计出了分页模板的表现形式,接下来让我们利用javascript这个强大的操盘手来粘合其余的部分吧。

第一步,我们需要设计一个抽象的基类,来实现代码复用(js的OO,不就是为了这个么,还有就是方便管理代码而已)。 首先,我们搞个命名空间来管理基类及其子类:
 

js 代码
  1. var Tbi = new Object();    //全局命名空间  

为啥叫“Tbi”?就不要问了,项目组的名称而已,呵呵:)接下来考虑我们的模板基类,他有什么特征呢?

1、需要持有一个到页面容器的引用,以便将自身加入页面流;

2、需要保存用户设定的每页显示条目数;

3、需要保存获取分页数据的服务器端地址(URL);

这些成员可以通过构造器来初始化,除此之外,为了避免数据更新发生混乱(这个在异步非缓存的时候需要注意),还要有一个全局异步对象来获取数据,但不需要通过参数提供。

定义了成员变量,模板对象还需要有可操作的方法:

1、构造函数:initialize,这个方法在构建Prototype风格构建类,实例化对象的时候会自动调用。另外有一些技巧下面会提到;

2、在数据获取过程中,显示等待信息的方法:_showMessage,这里只是简单显示一句话,可以按需替换成显示图片等;

3、显示模板初始页面的show方法(其中有两个故意设计的事件入口,供今后扩展使用):
     A、调用beforeShow,默认为空方法,什么也不做,可按需设定;
     B、调用_display方法,显示模板初始页面:
        a、调用2中的方法显示等待信息;
        b、调用_catchContent方法异步获取服务器数据。此方法可由所有模板共用,因此放到基类中达到代码重用。可以看到, 我们为了细粒度的控制Prototype的ajax封装,使用了transport属性。
        c、在成功返回服务器数据之后,addContent方法将把分页内容添加到容器中,分两步来做:
       (1) addPage方法作为回调函数添加模板主体——抽象方法,由子类实现。
       (2) addNavigation方法根据实际情况生成并添加分页导航——抽象方法,我将它在一个子类中实现然后演示混入(mixin),其实完全可以在基类中提供达到同样的代码复用效果。

js 代码
  1. /* 
  2.  * 模板基类 
  3.  */  
  4. Tbi.Template = Class.create();  
  5. Tbi.Template.prototype = {  
  6.     initialize: function(){  
  7.         this._init.apply(this,arguments);  
  8.     },  
  9.       
  10.     _init: function(wrap,items,catchUrl) {  
  11.         this.wrap = $(wrap);  
  12.         this.items = items;  
  13.         this.catchUrl = catchUrl;  
  14.         this.ajax = null;   //全局异步对象  
  15.     },  
  16.       
  17.     // 显示等待信息  
  18.     _showMessage: function(text){  
  19.         this.wrap.innerHTML =  text;  
  20.     },  
  21.       
  22.     // 显示模板默认页面  
  23.     show: function(){  
  24.         this.beforeShow(); //显示前事件处理入口  
  25.         this._display();     
  26.         this.afterShow(); //显示后时间处理入口  
  27.     },  
  28.       
  29.     // 显示前事件处理入口  
  30.     beforeShow: function(){},  
  31.       
  32.     _display: function(){  
  33.         this._showMessage("正在获取数据,请稍等……");  
  34.         this._catchContent();  
  35.     },  
  36.       
  37.     //取得默认页面内容,由两个模板公用  
  38.     _catchContent: function(){  
  39.         if(this.ajax){  
  40.             this.ajax.transport.abort();  
  41.         }  
  42.         this.ajax = new Ajax.Request(  
  43.             this.catchUrl,  
  44.             {  
  45.                 method: "get",  
  46.                 parameters: {"mode":this.mode,"items":this.items},  
  47.                 onComplete: this.addContent.bind(this)  
  48.             }  
  49.         );  
  50.     },  
  51.       
  52.     // 添加页面内容  
  53.     addContent: function(xmlhttp){  
  54.         this.addPage(xmlhttp);          //抽象方法,添加页面内容  
  55.         this.addNavigation();           //抽象方法,添加分页导航  
  56.     },  
  57.       
  58.     // 显示后事件处理入口          
  59.     afterShow: function(){}  
  60. }  

一些技巧:

为了实现在子类中覆盖initialize构造函数,我们只需要将基类的成员初始化工作委托给_init方法即可,下面会看到他的作用。

模仿事件处理机制,我们留下了两个事件入口beforeShow和afterShow,分别可以设置显示前事前和显示后事件。

Prototype为Function对象扩展了一个bind方法和bindAsEventListener方法,可以很方便的将函数上下文(this)切换为别的对象,这里,我们切换为模板对象。bind和bindAsEventListener的功用相似,但是有一点区别,用bind切换的方法,如果有自动传入的参数,比如事件对象event,那么这个参数将被自动传入函数参数列表的最后,而后者则是自动传入函数参数列表的最前面,这个技巧很常用。比如改写一下上面的代码,我给onComplete事件传入一个自定义参数tmyArg,使用两种不同的绑定方法(xmlhttp是Prototype自动传入回调函数的参数):

js 代码
  1. //用bind方法  
  2. this.addContent.bind(this,myArg)  
  3. //调用方式  
  4. this.addContent(myArg,xmlhttp)  
  5.   
  6.   
  7. //用bindAsEventListener方法  
  8. this.addContent.bindAsEventListener(this,myArg)  
  9. //调用方式  
  10. this.addContent(xmlhttp,myArg) 

第二步,实现静态模板类StaticTemplate。对于静态模板,其显示过程是这样的:向服务器发起一次异步请求,返回所有分页数据,服务器按照客户设定的每页显示数量来生成所有页,一次新传回客户端。每一页由一对tbody元素定义,然后通过css类名来讲所有的页面隐藏,最后由客户端js来控制显示页面,初始显示第一页。服务器返回的数据格式如下(只要结构一样就可以了,没有行、列数目的限制,这可以参看前一篇文章中的css设定了解,是可以随表格大小伸缩的),总条目数为6,分了两页:

xml 代码
  1. <table id='pages'>      
  2.     <caption>静态分页模板</caption>      
  3.     <thead>      
  4.         <tr>      
  5.             <th>标题一</th>      
  6.             <th>标题二</th>      
  7.             <th>标题三</th>      
  8.             <th>标题四</th>      
  9.             <th>标题五</th>      
  10.             <th>标题六</th>      
  11.         </tr>      
  12.     </thead>      
  13.     <tbody class='hidden'>      
  14.         <tr>      
  15.             <td>1</td>      
  16.             <td>1</td>      
  17.             <td>1</td>      
  18.             <td>1</td>      
  19.             <td>1</td>      
  20.             <td>1</td>      
  21.         </tr>      
  22.         <tr>      
  23.             <td>2</td>      
  24.             <td>2</td>      
  25.             <td>2</td>      
  26.             <td>2</td>      
  27.             <td>2</td>      
  28.             <td>2</td>      
  29.         </tr>      
  30.         <tr>      
  31.             <td>3</td>      
  32.             <td>3</td>      
  33.             <td>3</td>      
  34.             <td>3</td>      
  35.             <td>3</td>      
  36.             <td>3</td>      
  37.         </tr>      
  38.         <tr>      
  39.             <td>4</td>      
  40.             <td>4</td>      
  41.             <td>4</td>      
  42.             <td>4</td>      
  43.             <td>4</td>      
  44.             <td>4</td>      
  45.         </tr>      
  46.         </tbody>      
  47.           
  48.         <tbody class='hidden'>      
  49.         <tr>      
  50.             <td>5</td>      
  51.             <td>5</td>     
  52.             <td>5</td>      
  53.             <td>5</td>      
  54.             <td>5</td>      
  55.             <td>5</td>      
  56.         </tr>      
  57.         <tr>      
  58.             <td>6</td>      
  59.             <td>6</td>      
  60.             <td>6</td>      
  61.             <td>6</td>      
  62.             <td>6</td>      
  63.             <td>6</td>      
  64.         </tr>      
  65.     </tbody>      
  66. </table>   

StaticTemplate类中,addPage方法统计table中tbody元素的个数来确定总页数,并附加到对象上。以供addNavigation方法成分页导航信息:

js 代码
  1. this.pageTotal = this.wrap.getElementsByTagName("tbody").length;  

在addNavigation方法中,又一次用到了前面提过的bindAsEventListener技巧,并通过切换css类名来达到突出显示当前页码以及显示页面。

具体的StaticTemplate代码实现如下:

js 代码
  1. /* 
  2.  * 静态模板,一次请求全部数据 
  3.  */  
  4. Tbi.StaticTemplate = Class.create();  
  5. Tbi.StaticTemplate.prototype = Object.extend(new Tbi.Template(),  
  6.     {  
  7.         /* 
  8.          * 构造函数,实例化静态模板 
  9.          * 参数:父容器,每页显示条目数,数据获取地址
     
  10.          */  
  11.         initialize: function(wrap,items,catchUrl){  
  12.             this._init(wrap,items,catchUrl);  
  13.             this.mode = "static";  
  14.         },  
  15.           
  16.         // 实现父类抽象方法,向页面添加默认分页  
  17.         addPage: function(xmlhttp){  
  18.             this.wrap.innerHTML = xmlhttp.responseText;  
  19.             this.pageTotal = this.wrap.getElementsByTagName("tbody").length;  
  20.             var table = $("pages");  
  21.             var pages = $A(table.getElementsByTagName("tbody"));  
  22.             displayPage = pages[0];  
  23.             displayPage.className = "";  
  24.         },  
  25.           
  26.         // 实现父类抽象方法,向页面添加分页导航  
  27.         addNavigation: function(){  
  28.             if(this.pageTotal>1){  
  29.                 var navigation = document.createElement("div");  
  30.                 navigation.id = "navigation";  
  31.                 var context = this;  
  32.                 $R(1,this.pageTotal,false).each(  
  33.                     function(item){  
  34.                         var link = document.createElement("a");  
  35.                         link.href = "#";  
  36.                         link.onclick = context._changePage.bindAsEventListener(context,link);  
  37.                         link.appendChild(document.createTextNode(item));  
  38.                         if(item==1){  
  39.                             link.className = "active";  
  40.                         }  
  41.                         navigation.appendChild(link);  
  42.                     }  
  43.                 );  
  44.                 this.wrap.appendChild(navigation);    
  45.             }             
  46.         },  
  47.           
  48.         // 导航链接点击事件处理函数,切换页面内容,同时改变导航链接样式(突出显示当前页)  
  49.         _changePage: function(event,link){  
  50.             var activeLink = $$('#TMPwrap div a[class="active"]')[0];  
  51.             if(activeLink != link){  
  52.                 var pages = $$("#TMPwrap tbody");  
  53.                 var oldPage = pages.find(  
  54.                     function(item){  
  55.                         return item.className == "";  
  56.                     }  
  57.                 );  
  58.                 var newPage = pages[link.firstChild.nodeValue-1]; //inner系列属性不兼容ff
     
  59.                 oldPage.className = "hidden";  
  60.                 newPage.className = "";  
  61.                 activeLink.className = "";  
  62.                 link.className = "active";  
  63.             }  
  64.             Event.stop(event);    
  65.         }  
  66.     }  
  67. );  

这样,我们就完成了静态分页模板的设计和实现,下一步是实现动态异步的两种获取方式模板。

在前面两篇文章中,我们实现了静态表格分页模板,下面让我们继续讨论,如何实现另外两种数据获取方式的模板。要缓解服务器的压力,我们可以这样做:

1、显示初始页面也就是第一页的时候,我们构造好表头、标题和第一页的数据,并按照服务器返回的总页数生成导航链接,之后将不再更新这部分重复的内容;

2、对于每一页新内容,我们在点击页码链接的时候再动态向服务器获取数据,服务器根据所请求的页码返回特定页的内容,替换掉当前显示的页面数据,这样可以减少网络带宽的使用,毕竟每次只返回一页的内容;

3、对于数据更新频繁的分页系统,就按照上面的办法每次获取新数据;而对于不是很频繁的数据,我们提供一种客户端缓存机制来加快响应速度和减轻服务器压 力:通过一个全局的cache数组来存放服务器端之前返回的数据,在切换页面的时候,先查询缓存,如果没有命中,再向服务器发起异步请求,当响应到达的时 候,也会将此次数据存入缓存以供使用。

按照上面的分析,可以发现这两种数据获取方式的模板有很多相似的地方,不同的只是缓存与否,我们可以将他们抽象成同一个类来实现,下面是javascript代码,我将在后面就一些需要注意的地方做详细讲解:

js 代码
  1. /* 
  2.  * 异步模板,每次请求一个页面,并缓存浏览过的页面(可选) 
  3.  */  
  4. var mixin = new Tbi.StaticTemplate();   //用于mixin(混入)对象方法addNavigation  
  5. Tbi.AsyncTemplate = Class.create();  
  6. Tbi.AsyncTemplate.prototype = Object.extend(new Tbi.Template(),  
  7.     {  
  8.         initialize: function(wrap,items,catchUrl,option){  
  9.             this._init(wrap,items,catchUrl);  
  10.             this.mode = "async";  
  11.             //option为可选参数,设置初始显示页面以及是否缓存页面  
  12.             this.option = Object.extend({isCache:true},option);  
  13.             if(this.option.isCache){  
  14.                 this.cache = new Array();   //缓存数组;  
  15.             }  
  16.         },  
  17.           
  18.         // 实现父类抽象方法,向页面添加默认分页  
  19.         addPage: function(xmlhttp){  
  20.             var original = xmlhttp.responseText;  
  21.             var html = original.stripScripts();  
  22.             this.wrap.innerHTML = html;  
  23.             original.evalScripts();  
  24.         },  
  25.           
  26.         // mixin(混入)StaticTemplate类的同名方法,向页面添加分页导航  
  27.         addNavigation: mixin.addNavigation, // 方法劫持,重用addNavigation添加分页导航  
  28.           
  29.         // 导航链接点击事件处理函数,切换页面内容,同时改变导航链接样式(突出显示当前页)  
  30.         _changePage: function(event,link){  
  31.             var activeLink = $$('#TMPwrap div a[class="active"]')[0];  
  32.             if(activeLink != link){  
  33.                 link.className = "active";  
  34.                 activeLink.className = "";  
  35.                 this._displayPage(link);      
  36.             }  
  37.             Event.stop(event);  
  38.         },  
  39.           
  40.         // 根据缓存标志切换页面内容  
  41.         _displayPage: function(link){  
  42.             var page = link.firstChild.nodeValue;  
  43.             // 如果是第一页,且进行缓存的话,直接切换页面元素的可见性  
  44.             if(page==1 && this.cache){  
  45.                 $("default").className = "";  
  46.                 $("swap").className = "hidden";  
  47.             }  
  48.             // 否则,根据缓存标志,切换显示其他页(包括非缓存的默认页)  
  49.             else{  
  50.                 $("default").className = "hidden";  
  51.                 var oldPage = $("swap");  
  52.                 swapPage = this._prompt("正在获取数据……");  
  53.                 $("pages").replaceChild(swapPage,oldPage);  
  54.                 this._showPage(page);                 
  55.             }  
  56.         },  
  57.           
  58.         // 显示提示信息  
  59.         _prompt: function(text){  
  60.             var swapPage = document.createElement("tbody");  
  61.             swapPage.id = "swap";  
  62.             var messageTr = document.createElement("tr");  
  63.             var messageTd = document.createElement("td");  
  64.             messageTd.colSpan = 100;  
  65.             messageTd.appendChild(document.createTextNode(text));  
  66.             messageTr.appendChild(messageTd);  
  67.             swapPage.appendChild(messageTr);  
  68.             return  swapPage;     
  69.         },  
  70.           
  71.         // 根据缓存标志显示页面  
  72.         _showPage: function(page){  
  73.             if(this.cache&&this.cache["page"+page]){  
  74.                 this._addPageContent(page);     //巧妙重用此方法达到添加页面的效果  
  75.             }  
  76.             else{  
  77.                 this._catchPage(page);  
  78.             }  
  79.         },  
  80.           
  81.         // 异步获取页面内容  
  82.         _catchPage: function(page){  
  83.             if(this.ajax){  
  84.                 this.ajax.transport.abort();  
  85.             }  
  86.             this.ajax = new Ajax.Request(  
  87.                 this.catchUrl,  
  88.                 {  
  89.                     method: "get",  
  90.                     parameters: {"mode":"page","items":this.items,"page":page},  
  91.                     onComplete: this._addPageContent.bind(this,page)  
  92.                 }  
  93.             );  
  94.         },  
  95.           
  96.         // 添加各分页,并根据缓存标志进行页面缓存  
  97.         _addPageContent: function(page,xmlhttp){  
  98.             var xml = xmlhttp?xmlhttp.responseXML:this.cache["page"+page];  
  99.             if(this.cache&&!this.cache["page"+page]){  
  100.                 this.cache["page"+page] = xml;  
  101.             }  
  102.             var oldPage = $("swap");  
  103.             var newPage = this._createPage(xml);  
  104.             $("pages").replaceChild(newPage,oldPage);  
  105.         },  
  106.           
  107.         // 由异步返回的xml,或者缓存数组中的xml信息生成页面内容  
  108.         _createPage: function(xml){  
  109.             var newPage = document.createElement("tbody");  
  110.             newPage.id = "swap";  
  111.             var trs = $A(xml.getElementsByTagName("tr"));  
  112.             var context = this;  
  113.             trs.each(  
  114.                 function(item,index){  
  115.                     var tr = document.createElement("tr");  
  116.                     var tds = $A(item.getElementsByTagName("td"));  
  117.                     tds.each(  
  118.                         function(item){  
  119.                             var value = item.firstChild?item.firstChild.nodeValue:"";  
  120.                             var td = document.createElement("td");  
  121.                             td.appendChild(document.createTextNode(value));  
  122.                             tr.appendChild(td);  
  123.                         }  
  124.                     );  
  125.                 newPage.appendChild(tr);              
  126.                 }  
  127.             );  
  128.             return newPage;   
  129.         }  
  130.     }  
  131. );  

一些技巧和说明:

1、AsyncTemplate由前面文章中的Template抽象父类派生而来,因此具有了父类的能力(与静态模板类StaticTemplate共用 _catchContent方法获取初始数据;具有可扩展的显示前、显示后事件入口),达到了代码复用的效果;

2、initialize构造函数中,参数和父类是一样的,只是多了一个可选参数option,用于设置缓存标志(默认为true,即开启缓存),如果开启缓存,则初始化一个缓存数组cache 。这里注意一下默认参数的设置方式,这在Prototype风格的代码中很常见:

js 代码
  1. this.option = Object.extend({isCache:true},option);  

3、接下来addPage实现父类的抽象方法,作为继承而来的_catchContent方法的回调函数,将处理服务器端返回的数据并显示模板初始页面, 也就是第一页。这里服务器端返回的数据格式如下,注意我们利用了Prototype的扩展evalScripts和stripScripts方法抽取执行 以及删除javascript脚本片段,以达到向客户端返回总页数的效果(后面将利用addNavigation方法和这个总数构建分页导航)

xml 代码
  1. <table id='pages'>      
  2.     <caption>动态分页模板</caption>       
  3.     <thead>       
  4.         <tr>       
  5.             <th>标题一</th>       
  6.             <th>标题二</th>       
  7.             <th>标题三</th>       
  8.             <th>标题四</th>      
  9.             <th>标题五</th>      
  10.             <th>标题六</th>       
  11.         </tr>       
  12.     </thead>       
  13.     <tbody id='default'>       
  14.         <tr>       
  15.             <td>1</td>       
  16.             <td>1</td>       
  17.             <td>1</td>       
  18.             <td>1</td>       
  19.             <td>1</td>       
  20.             <td>1</td>       
  21.         </tr>    
  22.         <tr>       
  23.             <td>2</td>       
  24.             <td>2</td>       
  25.             <td>2</td>       
  26.             <td>2</td>       
  27.             <td>2</td>       
  28.             <td>2</td>       
  29.         </tr>    
  30.         <tr>       
  31.             <td>3</td>       
  32.             <td>3</td>       
  33.             <td>3</td>       
  34.             <td>3</td>       
  35.             <td>3</td>       
  36.             <td>3</td>       
  37.         </tr>    
  38.         <tr>       
  39.             <td>4</td>       
  40.             <td>4</td>       
  41.             <td>4</td>       
  42.             <td>4</td>       
  43.             <td>4</td>       
  44.             <td>4</td>       
  45.         </tr>       
  46.     </tbody>       
  47.     <tbody id="swap" class="hidden">       
  48.         <!--新页面交换区-->  
  49.     </tbody>               
  50. </table>       
  51. <script type="text/javascript">       
  52.     template.pageTotal = 5;       
  53. </script>   

这里的tempate是页面上实例化的动态模板对象名称,我们为其添加了一个pageTotal属性,以达到和静态模板类相似的效果,共用

js 代码
  1. // mixin(混入)StaticTemplate类的同名方法,向页面添加分页导航  
  2. addNavigation: mixin.addNavigation, // 方法劫持,重用addNavigation添加分页导航  

mixin是我们前面实例化的一个addNavigation方法添加导航。(如果觉得耦合太紧,可以修改一下将其赋值给window,两个类都用window对象的同名属性来调用addNavigation生成导航,我觉得没有什么必要)

4、addNavigation方法,我们演示了如何混入(mixin)三方对象的实例方法来达到多继承的目的(这个方法完全可以抽象到父类Template中),其原理就是javascript的函数上下文是动态切换的:
StaticTemplate对象:

js 代码
  1. var mixin = new Tbi.StaticTemplate();   //用于mixin(混入)对象方法addNavigation  

用这种方式,我们可以给当前类混入任何三方对象的方法,javascript的灵活性在这里展现得淋漓尽致:)

5、其他的代码就不详细介绍了,大家自己看。值得一提的是_catchPage方法中:

js 代码
  1. onComplete: this._addPageContent.bind(this,page)  

我们又一次用到了bind方法,这里传入的page参数,在函数调用的时候将放到自动传入的xmlhttp参数之前:

js 代码
  1. _addPageContent: function(page,xmlhttp){  
  2.     var xml = xmlhttp?xmlhttp.responseXML:this.cache["page"+page];  
  3.     if(this.cache&&!this.cache["page"+page]){  
  4.         this.cache["page"+page] = xml;  
  5.     }  
  6.     var oldPage = $("swap");  
  7.     var newPage = this._createPage(xml);  
  8.     $("pages").replaceChild(newPage,oldPage);  
  9. }  

另外我们在_showPage中重用_了addPageContent方法,实现了_addPageContent的双重身份:作为回调函数添加新页面内容,或者读取缓存显示页面内容,javascript的代码重用又得以体现:)

6、服务器端返回的新页面数据,由于在IE中不能用inner系列属性直接设定(ie中出了td元素,对于其他的元素,inner系列属性都是只读的,汗一个),因此数据传输不能以内容为中心返回html,而只能返回xml格式:

xml 代码
  1. <tbody>       
  2.     <tr>       
  3.         <td>5</td>       
  4.         <td>5</td>       
  5.         <td>5</td>       
  6.         <td>5</td>       
  7.         <td>5</td>       
  8.         <td>5</td>       
  9.     </tr>       
  10.     <tr>       
  11.         <td>6</td>       
  12.         <td>6</td>       
  13.         <td>6</td>       
  14.         <td>6</td>       
  15.         <td>6</td>       
  16.         <td>6</td>       
  17.     </tr>       
  18.     <tr>       
  19.         <td>7</td>       
  20.         <td>7</td>       
  21.         <td>7</td>       
  22.         <td>7</td>       
  23.         <td>7</td>       
  24.         <td>7</td>       
  25.     </tr>       
  26.     <tr>       
  27.         <td>8</td>       
  28.         <td>8</td>       
  29.         <td>8</td>       
  30.         <td>8</td>       
  31.         <td>8</td>    
  32.         <td>8</td>    
  33.     </tr>    
  34. </tbody>    

7、接收到服务器传回的上述xml数据后,不能直接用document.getElementsByTagName("tbody")[0]来获得 tbody后直接append到页面,貌似浏览器并不认为这是一个合法的HTML DOM,即使的确是个tbody,汗!只能迭代读取每一行数据来生成,郁闷。这里又遭遇了一个IE的bug:替换页面上的tbody元素,如果先 remove掉已有的,再append新的,如此重复操作,将导致IE发生非法操作,但firefox却一切正常。解决办法是用dom提供的 replaceChild方法来替换,就没有问题了,奇怪。

至此,我们的动态模板也实现了,接下来将讲解服务器端的groovy简单实现,以及示例数据库,请参看: AJAX表格分页模板(续):服务器端实现

最后我要发一发牢骚:javaeye的代码编辑器真的是bug多多,太郁闷了,我只是想编辑这篇文章,添加下一页的导航,居然搞得数据丢失了。另外,在编 辑已有文章的时候,如果有xml代码,则代码格式必定混乱!每个标签都会缺失关闭符号(左尖括号和斜杠)。而且,不认识Groovy代码,我只能选择 xml来显示,唉,又一个让我失望的blog系统……

 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号