这是我写的关于列表组件的第3篇博客。前面的相关文章有:css
1. 列表组件抽象(1)-概述html
2. 列表组件抽象(2)-listViewBase说明git
本文介绍列表组件中我对分页和排序的抽象思路。github
先来讲分页,由于以前写过一篇简单封装分页功能pageView.js,此次封装分页时的思路基本与那篇博客的想法彻底同样,只不过考虑到我要写的列表组件,还有其它的分页形式,好比点击加载更多进行翻页,基于浏览器标准的scroll事件进行翻页,基于iscroll插件派发的scroll事件进行分页。因而我在该文的基础上进一步抽象,将分页的一些公共逻辑提炼到一个基类中,仅仅将UI与分页控制的逻辑留给子类实现,这样可以最大程度地简化代码。这个基类的最终实现是:https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/base/pageViewBase.js,它其实就是简单封装分页功能pageView.js这篇博客中pageView.js分离出来的,因此若是想要去了解这个文件的说明,能够访问以前的那篇博客。数据库
当我把分页的一些公共逻辑抽象到pageViewBase以后,在简单封装分页功能pageView.js这篇文章中的pageView.js就会变得特别简洁,我最终的实现是:https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/simplePageView.js。当你查看pageViewBase.js的源码和simplePageView.js的源码,会发现它们合起来就是简单封装分页功能pageView.js里面的pageView.js。json
其它的分页组件实现有:数组
基于浏览器标准的scroll事件进行翻页,https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/scrollPageView.js浏览器
基于iscroll插件派发的scroll事件进行分页,https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/iscrollPageView.js数据结构
这两个分页组件其实跟simplePageView没什么大的不一样,只是分页使用到的事件不一样,另外就是因为涉及到滚动翻页,因此还有一个什么时候进行自动翻页的判断问题,这个判断问题我会在后面的文章介绍滚动分页列表组件的时候再来讲明。oop
再来看排序。
相比之下,排序会比分页麻烦一些。作排序管理的目的在于,列表为用户提供数据的时候,为了更有针对性的查看数据,用户通常会但愿可以主动地控制列表的排序规则,因此得考虑排序管理的功能,以便列表可以实现自定义的排序。在用户作了排序操做以后,咱们须要告诉后台当前排序操做结果对应的排序字段以及每一个字段对应的排序值。因为有可能有多列排序的状况,因此传递排序参数时,还得按排序操做时的顺序,组织好排序字段的顺序,以便后台可以按照用户的操做结果,来进行排序处理。相似下面这样的数据结构就能够正确地反映一个排序操做的结果:
[ { "field":"name", "value":"asc" }, { "field":"contact", "value":"desc" } ]
字段在数组中的前后关系便可表明排序时的前后关系。只要可以获得这样一个结构,就能把转成json格式的字符串传递给后台进行处理。最终这个数据结构对应到数据库中的排序规则时,就是这样的:
order by name asc, contact desc
从排序操做上来讲,常见的table插件是这么作的:
1. 若是仅仅是鼠标单击排序列,那么执行的就是单列排序操做。只要按照 不排序->升序、升序->降序、降序->不排序的切换规则,在鼠标单击排序列以后,改变该列对应的排序字段的排序方式,而后触发查询便可。传递到后台时,排序参数最多包含一个字段。
2. 若是在鼠标单击排序列以前,用户先摁住了shift键,再作点击操做,此时用户执行的就是多列排序操做,在shift键摁住期间,先点击的排序列对应的字段在排序结果中的顺序靠前,后点击的靠后。单个排序列仍是按照 不排序->升序、升序->降序、降序->不排序的切换规则来更改自身的排序方式,可是在单击完以后并不会当即触发列表查询,而是要等到shift键释放以后,再来查询。传递到后台时,排序参数可能包含多个字段。
按照前面的这个需求,个人实现思路时:先把排序参数的管理和排序操做的控制分开,写成两个组件;排序参数的管理组件仅负责排序字段的数据这一层级的控制,不与任何UI打交道;排序操做的管理组件负责与DOM交互,响应用户的键鼠操做,内部实例化一个排序参数管理的组件,利用这个组件实例来完成对排序字段的修改。这么作的好处在于将数据与UI分离,其实也就是表现与行为分离,简化UI层的逻辑,让代码看起来更加清晰。
最后考虑到不一样的列表组件,可能有不一样的排序UI控制逻辑,因此也决定把排序组件抽象出一个基类,像pageViewBase同样,把一些排序组件公共的逻辑出现出来,好比事件监听,启用禁用以及排序参数管理组件的实例化等。最终我获得了如下2个核心的排序组件相关的文件:
排序参数管理组件:https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/base/sortFields.js
排序控制管理组件的基类:https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/base/sortViewBase.js
下面我把这两个文件的一些要点一一说明。
先说sortFields。
这个文件比sorViewBase还长,可想而知,若是我把sortFields的逻辑不分离,直接写在sortViewBase里面,sortViewBase的复杂性确定会增长很多。为了了解这个组件的做用,我先用几段简单的代码来演示它的用法,虽然在实际使用中,这个组件并不须要直接实例化,可是它仍是能够直接实例化的,否则就没法为sortView组件所用了。
经过下面的方式来实例化一个sortFields的组件。
var sf = new SortFields({ config: [ {field: 'name', value: ''}, {field: 'contact', value: 'desc', order: 2}, {field: 'email', value: 'asc', order: 1} ], //排序状态改变的事件回调 onStateChange: function(e, data){ console.log('field[' + data.field + '] sort state is ' + data.value); }, //排序开始的事件回调 onSortStart: function(e){ console.log('sort start->'); }, //排序结束的事件回调 onSortEnd: function(e){ console.log('<-sort end'); }, //排序值改变的事件回调 onSortChange: function(e){ console.log('sort value change! new value is:'); console.log(JSON.stringify(this.getValue())); }, });
先说config这个option,其它的介绍后面的用法再补充。config用来配置排序管理组件要管理的排序字段。用数组的形式来配置多个排序字段,单个排序字段的排序定义用一个js的字面量对象来配置。用field属性来定义排序字段的名称;用value属性来配置该字段初始化时的排序方式;用type属性来配置该字段的数据类型,如string,int等,这个有可能在后台会须要;用order属性来配置该字段在排序规则中的初始化位置。如以上config,在初始化后,实际上对应的排序规则就是:
order by email asc, contact desc
为啥是email在前,contact在后面,这个就是order属性的做用了。
sortFields组件提供了一个getConfig的实例方法,这个方法返回全部排序字段的当前状态:
在经过后面要介绍的changeState方法,改变了单个排序字段的排序方式后,咱们能够在其它位置经过调用getConfig方法,获取排序字段最新的状态,从而更新UI:
好比simpleSortView里面的render方法就是这么作的:
render: function () { var that = this, opts = this.options; //根据sortFields的当前状态,从新渲染全部排序项 this.sortFields.getConfig().forEach(function (fieldDef) { var $target = that.$sort_items.filter('[data-field="' + fieldDef.field + '"]'); $target.removeClass([ opts.sortAscClass, opts.sortDescClass ].join(' ')); if (fieldDef.value !== 'no') { $target.addClass( fieldDef.value == 'asc' ? opts.sortAscClass : opts.sortDescClass ); } }); }
sortFields提供了getValue方法,能够获得当前的排序结果,这个方法基于getConfig实现,过滤掉不排序的字段,同时按order属性对getConfig返回的数组进行排序:
getValue: function () { return this.getConfig().filter(function (def) { return def.value !== 'no'; }).sort(sortByOrder); },
sortFields最重要的方法是changeState(fieldName, multiple),这个方法用来改变某个排序字段的排序方式,它接收两个参数,第一个参数表明字段的名称,第二个参数表示是否进行多列排序。
这个时候再来补充说明下前面几个事件option的详细内容:
当咱们在UI层进行排序操做时,只有第一个字段在调用changeState时,会触发sortStart事件,也就是onSortStart那个回调,表示排序开始;
每次调用changeState方法,都会触发sortStateChange事件,也就是onStateChange那个回调,表示某个字段的排序方式改变;
若是是单个字段排序,在changeState方法最后会主动调用endSort方法来结束排序,在endSort方法内部,会判断当前全部的排序状态以及顺序与排序操做前的状态顺序是否有变化,若是有变化则触发sortChange事件,也就是onSortChange那个回调;
若是是多个字段排序,在changeState方法最后就不会主动调用endSort方法。由于对sortFields组件来讲,多列排序的时候,它根本不知道何时结束排序,因此必须由UI层主动调用endSort方法,好比说在shift键释放的时候;
在endSort方法最后,会触发sortEnd事件,也就是onSortEnd那个回调,表示排序结束。
下面先看经过这个方法进行单列排序的演示:
sf.changeState('email'); VM601:14 sort start-> VM601:10 field[email] sort state is desc VM601:22 sort value change! new value is: VM601:23 [{"field":"email","value":"desc","order":1,"type":"string"}] VM601:18 <-sort end
(VM601都是console里面复制的时候带出来的,不用关注~)从这个演示能看到,全部回调都被触发,而且getValue方法返回了changeState最新的排序结果。
再来看看多列排序的状况:
sf.changeState('contact',true); VM601:14 sort start-> VM601:10 field[contact] sort state is asc sf.changeState('name',true); VM601:10 field[name] sort state is asc sf.endSort() VM601:22 sort value change! new value is: VM601:23 [{"field":"contact","value":"asc","order":1,"type":"string"},{"field":"name","value":"asc","order":2,"type":"string"}] VM601:18 <-sort end
在这个演示中,我先后改变了两个字段的排序方式,先改变的是contact,后改变的是name,最后经过endSort结束了此次多列排序,而后在onSortChange回调中,咱们看到了跟咱们排序操做一致的排序结果。同时也能够看到sortStart等几个事件回调,在多列排序操做时的执行状况,跟我前面的说明是彻底一致的。
到这里为止,我说明了sortFields的实现思路和使用方法,根据以上内容再去阅读源码,应该就比较好理解了。
接下来介绍sortViewBase.
其实这个类就很简单了。代码结构跟以前的listViewBase和pageViewBase都一致。
defaults定义以下:
var DEFAULTS = { config: [],//排序字段的配置 sortParamName: 'sort_fields',//排序参数名称 onChange: $.noop,//排序改变时的回调 onInit: $.noop,//初始化完毕的回调 };
这个config是在内部实例化sortFields组件的时候用到的,sortParamName是在为列表组件提供排序参数时用到的,这个参数名将会用来传递到后台,onChange也是提供给列表组件使用的,外部在此回调内触发列表查询。
这个基类的实现也有用到模板方法。init方法实现跟前面的博客介绍的组件差很少,它在中间的代码实例化了前面写的sortFields组件:
//初始化一个内部的排序管理组件SortFields的实例 var _render = $.proxy(this.render, this); this.sortFields = new SortFields({ config: opts.config, onReset: _render, onStateChange: _render, onSortChange: function (e) { that.trigger('sortViewChange' + that.namespace); } }); this.render();
render是sortView组件的一个实例方法,在sortFields重置,排序状态改变的时候,都会调用这个render方法来实现UI层的更新。
而后这个基类还提供了enable和disable方法,作启用和禁用的控制。
最后来介绍sortViewBase的一个实现:simpleSortView。
在demo中,listView_1.html里面,下面这个UI内容,就是simpleSortView组件的实例:
tableView.html里面,整个表头就是一个simpleSortView的实例:
simpleSortView的defaults以下:
var DEFAULTS = $.extend({}, SortViewBase.DEFAULTS, { //排序项的选择器 sortItemSelector: '.sort_item', //升序状态的css类名 sortAscClass: 'sort_asc', //降序状态的css类名 sortDescClass: 'sort_desc' }),
sortItemSelector用来筛选那些与每一个排序字段对应的元素,后面两个cssClass是做为排序状态类来使用的。把这些定义成option也是为了增长组件的灵活性。
simpleSortView其实就是作了些事件绑定来控制排序操做,以及UI渲染的逻辑。
rende方法在前面已经说明过了,须要补充一下的就是,simpleSortView为了可以将DOM元素与排序字段对应起来,必须在DOM元素加些特定的属性来标识,这里我用的是data-field属性。只要把某个DOM元素的data-field属性的值,配置成排序字段的名称,它们就关联起来了。
排序操做的控制逻辑其实也很是简单:
bindEvents: function () { //子类在实现bindEvent时,必须先调用父类的同名方法 this.base(); var that = this, opts = this.options; var rnd = this.namespace_rnd; //在事件后面增长随机数的目的是防止$document的事件触发冲突 //结合namespace跟rnd,就至关于给document的事件添加了两个命名空间 //这样即便同一个页面中有多个SimpleSortView的实例,互相之间也不会有事件冲突的影响 $document.on('keydown' + this.namespace + '.' + rnd, function (e) { if(that.disabled) return; if (e.which == 16) { //shift键按下的时候,表示要进行多列排序 that.multiple = true; } }).on('keyup' + this.namespace + '.' + rnd, function (e) { if(that.disabled) return; if (e.which == 16 && that.multiple) { that.multiple = false; //shift键抬起的时候,调用sortFields的实例的endSort方法,结束多列排序 that.sortFields.endSort(); } }); this.$element.on('click', opts.sortItemSelector, function () { if(that.disabled) return; that.sortFields.changeState($(this).data('field'), that.multiple); }); },
也就是按前面的单列和多列排序操做的需求实现而已。因为用到了$document这种公共的DOM对象来注册事件,因此为了不事件冲突,在已有的命名空间的基础上,又加了一个随机数做为新的命名空间。若是没有这个,当页面内包含多个simpleSortView组件实例时,keydown和keyup事件就会冲突。
到此为止,分页组件和排序组件的一些要点也都介绍完了,但愿这些东西能帮助感兴趣的朋友理解个人思路。