你的位置:首页 > Java教程

[Java教程]列表组件抽象(1)


这次要介绍的是列表组件。为了写它,我花了有将近2周的晚上,才一点一点的把它写到现在这个程度。到目前为止,一共写了有17个文件,虽然没有覆盖到一些更复杂的场景,但是把我当时计划写这个组件的基本目的已经完成了。先给大家看看我最后写出来的文件情况:

image

也许有人会好奇,一个列表的功能怎么会写出这么多东西出来?关于这个问题的答案,我稍后再来总结,先让我描述下我写这些东西之前产生的想法。

1. 背景介绍

我是去年5月份在上家公司开始做的前端开发,在那个公司,我花了2周的时间,尽力写了一套js的组件,用于我们当时的一个管理系统的开发。大家都知道,在管理系统里面,最常见的两种页面类型无非就是列表页面和编辑页面。拿列表页面来说,它里面用到的列表组件通常基于table进行开发的,当时考虑过jquery.datable这样的插件,后来放弃了。原因是:jquery.datatable有很多多余的东西,我不需要;它的框架在使用时候的思路,跟我以前使用的其它公司开发平台里面列表组件完全不同,我觉得我以前使用的组件思想更清晰简单。所以我就凭自己当时的能力开发出了一个能满足以下一些功能的table组件:

1. 分页;2. 列宽拖拽;3. 序号列自动生成;4. 单选和多选;5. 列表头固定,以便在列表区域滚动的时候,列表头不会受滚动的影响;6. 树形列表;7. 列表各种事件等。

后来我不单是做管理系统了,还做了更面向普通用于的网站类应用和移动端应用。这个时候又根据产品的需求开发出了三个另外的列表组件:第一个是带分页工具栏的列表组件,这个组件相当于就是前面那个table组件的缩小版,只不过html结构上不再使用table,而且也不需要拖拽,树形控制那样的功能了;第二个在第一个的基础上去掉了分页工具栏,改成了利用window的滚动事件进行滚动翻页,当然为了留一个后路,这个组件提供了加载更多这样的手工进行下一页的按钮;第三个使用于移动端,由于移动端scroll事件必须得等到屏幕的滚动操作完全停止下来以后才会触发,所以不能直接利用window的滚动事件,而改用了iscroll插件来模拟滚动,同时利用它提供的scroll事件来实现更加快速响应的滚动翻页功能。

其实在我后来写第二个组件的时候,我当时就在考虑:不管是后台的列表也好,还是前台的列表也好,除了表现出来的样子不一样,它们在发送请求的方式,解析响应的方式,以及列表渲染的方式应该都是差不多的,而且在后台的列表中,类似分页排序这样的功能,在前台的列表里面也有较为常见的使用场景,既然如此,这些相似的东西,如果能抽象到一个父类里面,然后由前后台的列表组件去实现各自独有的部分,这样的代码的结构会不会更加简练一点。因为当时后台的那个列表组件,我已经写了1000多行了,自己有时候想改个东西,都要滚来滚去地阅读以前写的代码,十分不便。

再拿后面的三个组件来说,滚动分页与不滚动分页组件的区别,仅仅在于分页的控制不同而已,如果把分页这一个部分完全丢出去,那么剩下的部分,就不需要在两个组件里面都去实现了,只用在一个类里面实现一次。iscroll与window的scroll,也仅仅只是滚动事件的发布者不同而已。如果我提供两个不同的滚动翻页的组件,一个使用window scroll实现,一个使用iscroll实现,然后由列表组件去决定要使用哪个分页组件,那么代码会更加简洁。

假如按以上的思路实现,我之前写的那些代码,可以得到以下这些方面的改进:

1. 通过继承解决掉代码重复问题,而且增强了代码的可维护性;

2. 通过职责分离,比如把分页功能,排序功能从组件中分离出来,然后组件内部采用注入的方式进行使用,这样一个列表想要更换分页、排序等扩展功能就会变得很容易。

所以这也就是我写前面截图那些文件的由来。至于为啥会有这么多个,这都是由于抽象出了一些公共的基类以及把分页,排序等功能从列表组件内分离出来之后的结果,而且这些文件实际上包含了前面提到4种列表的所有功能,所以总的文件数就比较大了。在实际使用中,要使用某一个类型的列表的时候,基本上只要使用simpleListView, tableView , scrollListView , iscrollListView即可。另外拆分成这么多文件之后,每个文件就变得很小了,基本上都是200行左右,没有一个超过400行的,更容易阅读理解。

由于这一次的内容比较多,这篇文章无法详细的介绍所有内容,只能先把文件之间的结构以及应用场景介绍一下,后面几篇文章会针对一些我觉得有必要进行解释说明的内容进行补充。欢迎感兴趣的朋友继续关注后面的文章,我会在近期陆续总结出来,好在最近的工作应该不会像之前那么忙。源代码已经上传到github,可通过下面几个地址来获取组件源码以及demo的源码:

组件源码:

https://github.com/liuyunzhuge/blog/tree/master/form/src/js/mod/listView

demo源码:

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/app/listView_1.js

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/app/listView_2.js

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/app/listView_3.js

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/app/listView_4.js

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/app/tableView.js

接下来介绍下各个文件的具体作用。

2. 文件结构

1. base/listViewBase

它是所有列表组件的基类,基本上所有公共的事情,都是在它里面处理的,比如排序组件初始化,分页组件初始化,模板引擎管理组件初始化,以及请求发送和请求解析等。每一个具体的列表管理组件都需要继承它。在这个类里面使用了模板方法的设计模式,以及在请求数据的前后,添加了多个事件回调,目的是为了子类能够进行灵活的个性化扩展。

2. base/pageViewBase以及simplePageView

pageViewBase是所有分页组件的基类。分页组件不管内部如何实现,其实对外需要提供的接口或者事件都是相同的。对于列表组件来说,只需要知道分页组件什么时候分页参数发生改变了而已,然后在改变的时候,带上最新的分页参数重新请求数据。这个类从简单封装分页功能pageView.js这篇文章中提到的pageView.js里面抽象出来。那篇文章介绍的pageView.js的功能,现在只需要定义一个继承pageViewBase的类,然后把pageViewBase未实现的那些具体逻辑添加进去即可,也就是现在的更加简洁的simplePageView。

3. base/sortViewBase和sortFields

sortViewBase是所有排序组件的基类。排序组件的实现思路,其实跟分页组件的思路很像,对于列表组件来说,只需要知道排序组件什么时候排序状态发生了变化即可。就像很多的table插件一样,一般列表都是单击某列的标题即可使得列表的数据按该列进行不同方式的排序,排序组件负责实现的就是这些排序方式切换的操作逻辑。由于排序组件,最终要给列表组件提供排序参数,所以又把排序参数的管理分离出来,写成了sortFields类。这样做的目的主要是为了简化sortViewBase的实现。在编程中,把数据的逻辑与UI的逻辑分开,往往是写出更好代码的关键。

4. base/tplBase和mustacheTpl

base/tplBase是模板引擎管理组件的基类。模板引擎管理组件用于渲染列表的数据。它声明了两个接口,compile和render,用来进行模板的编译和模板的渲染。之所以要这么写,而是因为假如有人想采用这一套代码的话,也许我这里面用到的模板引擎并不是他们团队人员所熟练使用的,所以为了能够更加灵活的适用其它模板引擎,我就把模板引擎的使用给抽象了。同时由于我在代码中都是使用mustache作为模板引擎,所以我也提供了继承tplBase的mustache版本的实现mustacheTpl。

5. simpleListView 和simpleSortView

看名字也能知道它们的作用了。simpleListView 继承自listViewBase,simpleSortView继承自sortViewBase。simpleListView 实现最简单的分页列表管理组件。simpleSortView给这个列表管理组件提供排序管理的功能。

6. scrollListView和scrollPageView

scrollListView继承自listViewBase,scrollPageView继承自pageViewBase。scrollListView实现相对于浏览器窗口或者某一个DOM元素进行滚动翻页的列表组件,这个列表组件与simpleListView的区别不仅仅是翻页的不同,同时在数据的渲染和UI交互上也有区别,毕竟翻到下一页的时候,需要一些更加友好的加载提示,以及不能删除之前已加载的数据内容。scrollPageView用来实现加载更多的翻页功能。

7. iscrollListView和iscrollPageView

跟前面不一样的是iscrollListView继承自scrollListView,毕竟它跟scrollListView是有很多共性的,不过它比scrollListView多了一个iscroll实例的管理功能,因为它要用iscroll插件。iscrollPageView提供给iscrollListView针对iscroll插件的加载更多的翻页功能。

8. tableView

它继承自scrollListView。用来实现基于table的列表组件。支持单多选,支持获取选中行的数据,支持按索引获取行的数据,支持表头固定等。它是所有列表组件中相对复杂的一个。但实际上只是功能多,逻辑并不复杂。

9. tableDrag和tableOrder

我把它们作为插件提供给tableView,分别为它提供列宽拖动以及序号列生成的功能。这么做的目的也是考虑到尽可能地简化tableView的功能。毕竟列宽拖拽这些功能都属于可选性质的。假如后面我想给tableView增加一个树形列表的管理功能,那么只要按照这两个插件的实现方式再写一个类似tableTree的插件即可。

10. tableDefault

这个纯粹是为了简化实例化tableView时,提供一些默认的配置,比如插件的配置等等。

以上就是这些文件的基本作用。下面会结合demo来演示四个列表组件的实际使用方式。

3. simpleListView

demo地址:http://liuyunzhuge.github.io/blog/form/dist/html/listView_1.html

demo文件:https://github.com/liuyunzhuge/blog/blob/master/form/src/js/app/listView_1.js

实际效果:

image

顶部打印了列表组件在请求数据时传递给后台的参数。排序组件由于涉及到可能有多个排序字段,所以用数组字符串进行传递,各个字段在数组中的顺序,代表它们在数据库中进行排序时的先后关系。

相关使用代码如下:

define(function (require) {  var $ = require('jquery'),    ListView = require('mod/listView/simpleListView'),    api = {      list: './api/pageView.json',    };  var list = new ListView('#blog_list', {    url: api.list,    tpl: ['{{#rows}}<li class="blog-entry">',      '  <a href="#" class="diggit">',      '    <span class="diggit-num">{{like}}</span>',      '    <span class="diggit-text">推荐</span>',      '  </a>',      '  <div class="cell pl15">',      '    <h3 class="f14 mb5 lh18"><a href="#" class="blog-title">{{title}}{{index}}</a></h3>',      '    <p class="pt5 mb5 lh24 g3 fix">',      '      <img src="{{avatar}}" alt="" class="bdc p1 w50 h50 l mr5">{{content}}</p>',      '    <p class="mt10 lh20"><a href="#" class="blog-author">{{author}}</a><span class="dib ml15 mr15">发布于 {{publish_time}}</span><a',      '        href="#" class="blog-stas">评论({{comment}})</a><a href="#" class="blog-stas">阅读({{read}})</a></p>',      '  </div>',      '</li>{{/rows}}'].join(''),    parseData: function (data) {      var start = list.pageView.data.start;      data.forEach(function (d) {        d.index = start;        start = start + 1;      });    },    pageView: {      defaultSize: 3    },    sortView: {      config: [        {field: 'name', value: ''},        {field: 'sales', value: 'desc', order: 2, type: 'int'},        {field: 'time', value: 'asc', order: 1, type: 'datetime'}      ]    },    beforeAjax: function () {      var html = [],        params = list.getParams(),        hasOwn = Object.prototype.hasOwnProperty;      for (var i in params) {        if (hasOwn.call(params, i)) {          html.push('<p>' + i + ' : ' + JSON.stringify(params[i]) + '</p>');        }      }      $('#log').html(html.join(''));    }  });  list.query();});

4. scrollListView

demo地址:

http://liuyunzhuge.github.io/blog/form/dist/html/listView_2.html

http://liuyunzhuge.github.io/blog/form/dist/html/listView_3.html#

demo文件:

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/app/listView_2.js

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/app/listView_3.js

这个之所以有两个demo,那是因为第一个demo是相对于window进行滚动翻页的,第二个demo是相对于某个DOM元素进行滚动翻页的。

实际效果这里就不展开了。毕竟这个demo要鼠标滚动操作才能看到更真实的效果。可通过前面的链接进行查看。

实际使用的代码也不展示了,因为跟前面的simpleListView差不多。

5. iscrollListView

demo地址:

http://liuyunzhuge.github.io/blog/form/dist/html/listView_4.html

demo文件:

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/app/listView_4.js

预览的话可以按F12切换的手机模拟器进行demo预览,iscroll毕竟是移动端用的东西,pc端也能遇到,要用鼠标拖动才能进行滚动,我没有启用滚轮操作。其它的跟前面差不多。

6. tableView

demo地址:

http://liuyunzhuge.github.io/blog/form/dist/html/tableView.html

demo文件:

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/app/tableView.js

实际效果:

image

tableView组件跟其它列表组件很大不同的一点是,它对html结构要求更加严格:

image

好在这个结构其实是非常清晰的。之所要弄成这个,跟表头固定的需求有关系,然后表头滚定又会带来其它相关的问题,这些我会在后面的文章再细说。

使用的方式是:

define(function (require) {  var $ = require('jquery'),    ListView = require('mod/listView/tableView'),    TableDefault = require('mod/listView/tableDefault'),    api = {      list: './api/tableView.json',    };  var list = window.l = new ListView('#table_view', {    multipleSelect: true,    heightFixed: true,    url: api.list,    tableHd: ['<tr>',      '  <th>序号</th>',      '  <th><input type="checkbox" class="table_check_all"></th>',      '  <th data-field="name" data-drag="false" class="sort_item">姓名 <i class="sort_icon"></i></th>',      '  <th data-field="contact" data-drag-min="100" data-drag-max="200" class="sort_item">联系方式 <i class="sort_icon"></i></th>',      '  <th data-field="email" class="sort_item">邮箱 <i class="sort_icon"></i></th>',      '  <th>昵称</th>',      '  <th>备注</th>',      '</tr>'].join(""),    colgroup: ['<colgroup>',      '  <col width="70">',      '  <col width="40">',      '  <col width="120">',      '  <col width="120">',      '  <col width="180">',      '  <col width="180">',      '  <col width="200">',      '</colgroup>'].join(""),    tpl: ['{{#rows}}<tr>',      '<td><span class="table_view_order"></span></td>',      '<td align="middle" class="tc"><input type="checkbox" class="table_check_row"></td>',      '<td>{{name}}</td>',      '<td>{{contact}}</td>',      '<td>{{email}}</td>',      '<td>{{nickname}}</td>',      '<td><button class="btn-action" type="button">操作</button></td>',      '</tr>{{/rows}}'].join(''),    sortView: {      config: [        {field: 'name', value: ''},        {field: 'contact', value: 'desc', order: 2},        {field: 'email', value: 'asc', order: 1}      ]    },    pageView: {      defaultSize: 20    },    plugins: TableDefault.plugins  });  list.$element.on('click','.btn-action', function(e) {    console.log(list.getRowData($(this).closest('tr').index()));  });  list.query();});

在demo代码中还可以看到,tableView组件使用colgroup和tableHd两个option来定义列宽描述以及表头内容的html模板,之所以不直接写在html里面,也是为了html的简洁考虑。如果放在js里面的话,反而更统一一些,毕竟表体部分的html模板就是js中进行定义的。

通过以上这些demo可以看到,虽然前面定义了很多文件,但是在使用的时候,这些组件用起来并不复杂,要是结合我之前写的那些跟form相关的组件,那么去做一些带查询列表的列表管理功能也不是很难,基本只需要list.query(appForm.getData())即可。欢迎感兴趣的朋友前去使用,这些都是我已经在实际项目中使用了以后感觉比较省时省力的方法,不比一些有名的jquery插件差。

7. 本文小结

总之,我是想到所有的列表的功能都具备相似性,不想在一个项目中,组件层面存在重复的代码,所以就写了这些东西,力求今后我在新项目中,能够将代码的重复度降到最低,方便维护跟扩展。本文也只是把我这段时间的成果起了个头,大体上介绍了下我写的这些列表组件的作用以及用法,但是里面还有很多的细节还没有完全说明,为了让人看得更加明白,我这几天会加速把相关内容总结出来。最后,希望这些东西不管是学习还是工作方面,都能给一些朋友带来实际的帮助。如果觉得里面有什么不好的、不对的,欢迎帮我指出。