这是我写的关于列表组件的第3篇博客。前面的相关文章有:
1.
2.
本文介绍列表组件中我对分页和排序的抽象思路。
先来说分页,因为之前写过一篇,这次封装分页时的思路基本与那篇博客的想法完全一样,只不过考虑到我要写的列表组件,还有其它的分页形式,比如点击加载更多进行翻页,基于浏览器标准的scroll事件进行翻页,基于iscroll插件派发的scroll事件进行分页。于是我在该文的基础上进一步抽象,将分页的一些公共逻辑提炼到一个基类中,仅仅将UI与分页控制的逻辑留给子类实现,这样能够最大程度地简化代码。这个基类的最终实现是:,它其实就是这篇博客中pageView.js分离出来的,所以如果想要去了解这个文件的说明,可以访问之前的那篇博客。
当我把分页的一些公共逻辑抽象到pageViewBase之后,在这篇文章中的pageView.js就会变得特别简洁,我最终的实现是:。当你查看pageViewBase.js的源码和simplePageView.js的源码,会发现它们合起来就是里面的pageView.js。
其它的分页组件实现有:
基于浏览器标准的scroll事件进行翻页,
基于iscroll插件派发的scroll事件进行分页,
这两个分页组件其实跟simplePageView没什么大的不同,只是分页使用到的事件不同,另外就是由于涉及到滚动翻页,所以还有一个何时进行自动翻页的判断问题,这个判断问题我会在后面的文章介绍滚动分页列表组件的时候再来说明。
再来看排序。
相比之下,排序会比分页麻烦一些。做排序管理的目的在于,列表为用户提供数据的时候,为了更有针对性的查看数据,用户一般会希望能够主动地控制列表的排序规则,所以得考虑排序管理的功能,以便列表能够实现自定义的排序。在用户做了排序操作之后,我们需要告诉后台当前排序操作结果对应的排序字段以及每个字段对应的排序值。由于有可能有多列排序的情况,所以传递排序参数时,还得按排序操作时的顺序,组织好排序字段的顺序,以便后台能够按照用户的操作结果,来进行排序处理。类似下面这样的数据结构就可以正确地反映一个排序操作的结果:
[ { "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个核心的排序组件相关的文件:
排序参数管理组件:
排序控制管理组件的基类:
下面我把这两个文件的一些要点一一说明。
先说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 descVM601: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 ascsf.changeState('name',true);VM601:10 field[name] sort state is ascsf.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事件就会冲突。
到此为止,分页组件和排序组件的一些要点也都介绍完了,希望这些东西能帮助感兴趣的朋友理解我的思路。