列表组件抽象(2):listViewBase说明

  • 内容
  • 评论
  • 相关
原文出处: 流云诸葛   

listViewBase是列表组件所有文件中最核心的一个,它抽象了所有列表的公共逻辑,将来如果有必要添加其它公共的逻辑,都可以考虑在这个类中处理。它主要做的事情包括:初始化,如排序组件初始化,分页组件初始化,模板管理引擎初始化,事件绑定和请求发送及处理等。这个文件看起来比较长,有300度行,但是非常好理解。下面我会把它的每个要点内容一一说明。

源码地址:https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/base/listViewBase.js

首先看看代码的整体结构。

列表组件抽象(2):listViewBase说明

注:代码中的EventBase是我原来写的一个组件,基于jquery,简单实现任意对象支持事件管理的功能【jquery技巧之让任何组件都支持类似DOM的事件管理】;Class也是我原来写的一个组件,用来支持面向对象思想的类的构造&继承【详解Javascript的继承实现】;Ajax也是一个简单的组件,对jquery的ajax进行了二次封装,以便ajax请求的管理更符合自己的思维习惯【对jquery的ajax进行二次封装以及ajax缓存代理组件:AjaxCache】。

listViewBase的整体结构跟我以前的写的组件基本一致,毕竟已经养成这个习惯了。DEFAULTS表示组件的默认options,它继承了EventBase来实现自身的事件管理。在组件类的静态成员上,绑定了DEFAULTS,是为了方便子类进行引用;定义了一个dataAttr的属性,它有两个作用:第一是作为data属性,在将组件实例绑定到相关DOM元素的jq对象上时用到:

列表组件抽象(2):listViewBase说明

第二是用于生成组件的事件命名空间,组件内所有的事件都会加上这个事件命名空间,以便不会产生事件冲突:

列表组件抽象(2):listViewBase说明
列表组件抽象(2):listViewBase说明

接着看看DEFAULTS的定义,我会挑主要的进行解释:

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
var DEFAULTS = {
    //接口地址
    url: '',
    //数据模板
    tpl: '',
    //ajax请求方法
    ajaxMethod: 'get',
    //判断成功的ajax
    isAjaxResSuccess: function (res) {
        return res.code == 200;
    },
    //从ajax返回中解析出数据
    getRowsFromAjax: function (res) {
        return res.data.rows;
    },
    //从ajax返回中解析出总记录数
    getTotalFromAjax: function (res) {
        return res.data.total;
    },
    //提供给外部解析ajax返回的数据
    parseData: $.noop,
    //提供给模板引擎,以便得到满足其要求的数据
    renderParse: function(paredRows){
        return {
            rows: paredRows
        }
    },
    //组件初始化完毕后的回调
    afterInit: $.noop,
    //ajax请求之前的事件回调
    beforeAjax: $.noop,
    //ajax请求之后的事件回调
    afterAjax: $.noop,
    //ajax请求成功的事件回调
    success: $.noop,
    //ajax请求失败的事件回调
    error: $.noop,
    //PageView相关的option,为空表示不采用分页
    pageView: {},
    //SortView相关的option,为空表示不采用排序管理
    sortView: false,
    //在调用query方法的时候,是否自动对SortView进行reset
    resetSortWhenQuery: false,
    //查询延时
    queryDelay: 0,
};

其中:

1)isAjaxResSuccess , getRowsFromAjax , getTotalFromAjax作用跟ajax的返回解析有关。通常做了自定义的ajax返回封装后,ajax的返回可能是类似这样的:

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
  "code": 200,
  "data": {
    "total": 237,
    "rows": [
      {
        "like": 2,
        "title": "博客标题博客标题",
        "avatar": "",
        "summary": "可以看到这个列表页其实是用到了很多语义化的命名的css类的,假如要用面向属性的命名方法来定义,就会变成下面这个样子:,可以看到这个列表页其实是用到了很多语义化的命名的css类的,假如要用面向属性的命名方法来定义,就会变成下面这个样子:",
        "author": "流云诸葛",
        "publish_time": "2016-06-05 08:53",
        "comment": 22,
        "read": "666"
      },
      {
        "like": 2,
        "title": "博客标题博客标题",
        "avatar": "",
        "summary": "可以看到这个列表页其实是用到了很多语义化的命名的css类的,假如要用面向属性的命名方法来定义,就会变成下面这个样子:,可以看到这个列表页其实是用到了很多语义化的命名的css类的,假如要用面向属性的命名方法来定义,就会变成下面这个样子:",
        "author": "流云诸葛",
        "publish_time": "2016-06-05 08:53",
        "comment": 22,
        "read": "666"
      },
      {
        "like": 2,
        "title": "博客标题博客标题",
        "avatar": "",
        "summary": "可以看到这个列表页其实是用到了很多语义化的命名的css类的,假如要用面向属性的命名方法来定义,就会变成下面这个样子:,可以看到这个列表页其实是用到了很多语义化的命名的css类的,假如要用面向属性的命名方法来定义,就会变成下面这个样子:",
        "author": "流云诸葛",
        "publish_time": "2016-06-05 08:53",
        "comment": 22,
        "read": "666"
      }
    ]
  }
}

以上这个ajax返回demo模拟了一个分页列表时某次ajax请求返回的数据,其中code属性为200表示这个ajax是成功的,ajax返回的数据集合存放在data.rows属性上,数据的总记录数存放在data.total属性上。有可能你的项目中,分页列表返回的数据结构跟这个不一样,但是对于列表组件来说,有三个要素是一个请求的返回中必须包含的:

a. 什么样的返回才是成功的;

b. 返回中的哪一部分表示当前请求的数据集;

b. 返回中的哪一部分表示当前数据类型的记录总数。

isAjaxResSuccess , getRowsFromAjax , getTotalFromAjax解决的就是这三个问题。我提供的这三个option的默认值都是按前面的那个json结构写的,如果你的项目中列表ajax请求不是这个json结构,只要改变这三个option的定义即可。

2)parseData和renderParse用于解析getRowsFromAjax返回的数据,以及为模板引擎提供它所需要的model对象。在一个列表ajax请求中,很有可能某些返回的数据不适合直接显示在页面里面,比如时间戳格式的字段,我们可能更需要把它转化为我们所习惯的日期格式字符串才行,这个时候只要利用parseData方法即可,这个方法接受getRowsFromAjax返回的数据作为唯一的参数。renderParse跟模板引擎有关系,拿mustache来说,如果我定义tpl的时候用的是下面类似的结构:

JavaScript

1
2
3
4
5
6
7
8
9
['{{#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(''),

意味着我在使用mustche渲染的时候,需要传入一个{rows: …}的model才行,这个model里面的rows是根据tpl里面的{{#row}}来确定的。默认情况下,我在定义tpl的时候,都使用rows作为遍历属性名,如果你不习惯用rows,那么可通过renderParse这个option来自定义要使用的遍历属性名。比如换成records:

JavaScript

1
2
3
4
5
renderParse: function(paredRows){
    return {
        records: paredRows
    }
},

3)afterInit等事件的作用在于组件实例可根据自身的需求场景,在这些事件派发的时候,添加额外的一些处理逻辑,而不会影响别的实例。

4)pageView跟sortView用来传递分页组件和排序组件实例化的时候,要传入的options。如果为false,则表示这个列表组件没有对应的分页组件或排序组件。

5)queryDelay如果大于0,那么就会延迟发送ajax请求,延迟时间就等于queryDelay设定的时间。

接下来看看一些关键的实例方法定义。

1)init方法

源码:

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
init: function (element, options) {
    var $element = this.$element = $(element),
        opts = this.options = this.getOptions(options),
        that = this;
 
    //初始化,注册事件管理的功能:EventBase
    this.base($element);
 
    //模板方法,方便子类继承实现,在此处添加特有逻辑
    this.initStart();
 
    //设置数据属性名称、命名空间名称
    this.dataAttr = this.constructor.dataAttr;
    this.namespace = '.' + this.dataAttr;
    //存放查询条件
    this.filter = {};
 
    //模板方法,方便子类继承实现,在此处添加特有逻辑
    this.initMiddle();
 
    //初始化分页组件
    //createPageView必须返回继承了PageViewBase类的实例
    //这里没有做强的约束,只能靠编码规范来约束
    this.pageView = this.createPageView();
    if (this.pageView) {
        //注册分页事件
        this.pageView.on('pageViewChange' + this.pageView.namespace, function () {
            that.refresh();
        });
    }
 
    //初始化模板管理组件,用于列表数据的渲染
    //createTplEngine必须返回继承了TplBase类的实例
    //这里没有做强的约束,只能靠编码规范来约束
    this.itemTplEngine = this.createTplEngine();
 
    //初始化排序组件
    //createSortView必须返回继承了SortViewBase类的实例
    //这里没有做强的约束,只能靠编码规范来约束
    this.sortView = this.createSortView();
    if (this.sortView) {
        //注册排序事件
        this.sortView.on('sortViewChange' + this.sortView.namespace, function () {
            that.refresh();
        });
    }
 
    //模板方法,方便子类继承实现,在此处添加特有逻辑
    this.beforeBindEvents();
 
    //绑定所有事件回调
    this.bindEvents();
 
    //模板方法,方便子类继承实现,在此处添加特有逻辑
    this.initEnd();
 
    $element.data(this.dataAttr, this);
 
    this.trigger('afterInit' + this.namespace);
},

这个方法其实很简单,就是按顺序做一些初始化的逻辑而已。稍微值的一提的是,为了让子类支持更灵活的扩展,这个方法在一些关键代码的前后都加了空方法,以便子类在父类的这些关键代码执行前后,插入自己的逻辑。createPageView用于子类返回分页组件的实例,如果返回了分页组件实例,会自动监听分页组件的相关change事件,并调用列表组件的refresh方法,以便根据最新的分页参数刷新列表。createSortView用于子类返回排序组件的实例,作用完全类似createPageView。

2. bindEvents方法

就是注册事件而已。不过子类在提供自己的bindEvents方法的时候,必须在它的bindEvents,通过this.base()调用父类的bindEvents方法。这里没有像init方法那样,增加很多空方法来处理。毕竟没有那么多个性化的位置。

3. getParams方法

返回列表的参数:

JavaScript

1
2
3
4
5
6
7
getParams: function () {
    //参数由:分页,排序字段以及查询条件构成
    return $.extend({},
        this.pageView ? this.pageView.getParams() : {},
        this.sortView ? this.sortView.getParams() : {},
        this.filter);
},

在请求发送时,会调用这个方法来获取要传递给后台的参数。

4. renderData方法

子类不用实现,但是子类会用到,它在内部调用模板引擎管理组件,来返回渲染之后的html字符串,子类在拿到这个字符串之后,可做DOM更新的操作。

5. refresh方法

代表列表刷新。仅在分页或排序改变的时候调用。

6. query方法

代表列表查询。这个方法跟refresh方法都在内部调用_query函数进行请求的处理,但是两个方法使用的场景不一样。

refresh方法基本上不影响参数,如果是分页refresh,那么参数中只有分页参数会变化;如果是排序refresh,那么参数中只有排序参数会变化;如果是其它refresh,所有参数都不变化,列表只是按当前条件重新请求一遍数据而已。

query方法不一样:它接收新的查询条件,用于更新原来的查询条件。并且它会重置分页排序组件,如果resetSortWhenQuery为true,它还会重置排序组件。query方法可以实现比较强大的列表查询功能。下面我会尽量详细介绍它的用法,由于没有查询条件的表单,所以我直接在控制台模拟一下了。你可以直接用http://liuyunzhuge.github.io/blog/form/dist/html/tableView.html这个页面进行操作,我把这个页面里面的的列表组件实例已经存放在window.l属性上,所以在控制台可以通过l这个全局变量拿到列表组件实例。

在此之前,我先假设有一个列表页面,放了两个查询条件,一个是按类型查,一个是按关键词查,当我们要执行搜索的时候,可以用下面的方式在给列表增加查询条件:

JavaScript

1
l.query({type: '1', keywords: 'ssss'})

查看ajax请求,可以看到新添加的请求参数:

JavaScript

1
2
3
4
5
6
7
rnd:0.3144281458900091
_ajax:1
page:1
page_size:3
sort_fields:[{"field":"time","value":"asc","order":1,"type":"datetime"},{"field":"sales","value":"desc","order":2,"type":"int"}]
type:1
keywords:ssss

如果此时改变其中一个查询条件的值:

JavaScript

1
l.query({type: '2'})

列表就会用新的查询条件请求数据:

JavaScript

1
2
3
4
5
6
7
rnd:0.15610846260036104
_ajax:1
page:1
page_size:3
sort_fields:[{"field":"time","value":"asc","order":1,"type":"datetime"},{"field":"sales","value":"desc","order":2,"type":"int"}]
type:2
keywords:ssss

如果在改变查询条件的同时,给query方法传递第二个参数,值为false:

JavaScript

1
l.query({type: '3'}, false)

会发现这次的列表请求中,已经没有了之前的那个keywords的参数:

JavaScript

1
2
3
4
5
6
rnd:0.09752677645742791
_ajax:1
page:1
page_size:3
sort_fields:[{"field":"time","value":"asc","order":1,"type":"datetime"},{"field":"sales","value":"desc","order":2,"type":"int"}]
type:3

因为query方法的第二个参数如果是false的话,列表组件在更新查询条件的时候,将采用替换而不是覆盖的方式处理。

前面说的query方法会重置分页组件或排序组件,是指在请求前会调用分页组件或排序组件实例的reset方法,以便还原排序和分页参数值为默认值。

最后再看核心一个函数定义:_query函数。

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//更新查询条件
//如果append为false,那么用newFilter替换当前的查询条件
//否则,仅仅将newFilter包含的参数复制到当前的查询条件里面去
function updateFilter(newFilter, append) {
    var filter;
 
    if (newFilter) {
        if (append === false) {
            filter = newFilter;
        } else {
            filter = $.extend({}, this.filter, newFilter);
        }
        this.filter = filter;
    }
}
 
//_query函数中关键的模板方法与事件的调用顺序:
//method: beforeQuery
//[method: queryCancel]
//event: beforeAjax
//1-成功:
//  method: querySuccess
//  event: success
//  method: afterQuery
//  event: afterAjax
//2-失败:
//  method: queryError
//  event: error
//  method: afterQuery
//  event: afterAjax
function _query(clear, newFilter, append) {
    var that = this,
        opts = this.options;
 
    if (!opts.url) return false;
 
    //调用子类可能实现了的beforeQuery方法,以便为该子类添加统一的一些query前的逻辑
    if (this.beforeQuery(clear) === false) {
        this.queryCancel(clear);
        return false;
    }
 
    if (clear) {
        //更新查询条件
        updateFilter.call(this, newFilter, append);
 
        //重置分页组件
        this.pageView && this.pageView.reset();
    }
 
    //禁用分页组件,防止重复操作
    this.pageView && this.pageView.disable();
 
    //还原排序组件
    this.sortView && opts.resetSortWhenQuery && this.sortView.reset();
 
    //触发beforeAjax事件,以便外部根据特有的场景添加特殊的逻辑
    this.trigger('beforeAjax' + this.namespace);
 
    if (opts.queryDelay) {
        var dtd = $.Deferred();
        var timer = setTimeout(function () {
            clearTimeout(timer);
            _request().done(function () {
                dtd.resolve.apply(dtd, arguments);
            }).fail(function () {
                dtd.reject.apply(dtd, arguments);
            });
        }, opts.queryDelay);
 
        return $.when(dtd);
    } else {
        return _request();
    }
 
    function _request() {
        return Ajax[opts.ajaxMethod](opts.url, that.getParams())
            .done(function (res) {
                //判断ajax是否请求成功
                var isSuccess = opts.isAjaxResSuccess(res),
                    rows = [],
                    total = 0;
 
                if (isSuccess) {
                    //得到所有行
                    rows = opts.getRowsFromAjax(res);
 
                    that.originalRows = rows;
 
                    //得到总记录数
                    total = opts.getTotalFromAjax(res);
 
                    //刷新分页组件
                    that.pageView && that.pageView.refresh(total);
 
                    var parsedRows = opts.parseData(rows);
                    if (!parsedRows) {
                        parsedRows = rows;
                    }
 
                    that.parsedRows = parsedRows;
 
                    //调用子类实现的querySuccess方法,通常在这个方法内做列表DOM的渲染
                    that.querySuccess(that.renderData(opts.renderParse(parsedRows)), {
                        clear: clear,
                        total: total
                    });
 
                    //触发success事件,以便外部根据特有的场景添加特殊的逻辑
                    that.trigger('success' + that.namespace);
 
                    _always();
 
                    //触发afterAjax事件,以便外部根据特有的场景添加特殊的逻辑
                    that.trigger('afterAjax' + that.namespace);
                } else {
                    _fail();
                }
            })
            .fail(_fail);
    }
 
    function _fail() {
        //调用子类实现的queryError方法,以便子类实现特定的加载失败的展示逻辑
        that.queryError({
            clear: clear
        });
 
        //触发error事件,以便外部根据特有的场景添加特殊的逻辑
        that.trigger('error' + that.namespace);
 
        _always();
 
        //触发afterAjax事件,以便外部根据特有的场景添加特殊的逻辑
        that.trigger('afterAjax' + that.namespace);
    }
 
    function _always() {
        //重新恢复分页组件的操作
        that.pageView && that.pageView.enable();
 
        //调用子类实现的afterQuery方法,以便子类实现特定的请求之后的逻辑
        that.afterQuery({
            clear: clear
        });
    }
}

这个函数源码较长,但是理解起来应该不会麻烦,因为它也跟init方法一样,纯粹是按顺序编写的一些逻辑。在这个函数里面调用了另外几个模板方法,派发了大量的事件。虽然看起来这些模板方法,跟事件的作用有些重合,其实它们的作用是完全不同的。模板方法是直接添加在类层面的,它可以为子类提供类级的扩展;而事件是由具体的实例派发的,所以它只能在给特定的实例添加扩展。

这些模板方法以及事件的触发顺序也比较关键,都是按照先调用模板方法,再派发事件的顺序来的,拿querySuccess方法与success事件来说,一定是先调用querySuccess方法,再派发success事件,这个原由也跟前面的类级扩展和实例级扩展的层次有关系。所有模板方法以及事件的调用关系,按照请求成功或失败分了2条线,我在注释中已经描述地很清楚了。

以上就是listViewBase这个基类的全部内容了。

接下来看看它的子类该如何实现,以simpleListView为例:

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
define(function (require) {
    var $ = require('jquery'),
        MustacheTpl = require('mod/listView/mustacheTpl'),
        SimplePageView = require('mod/listView/simplePageView'),
        SimpleSortView = require('mod/listView/simpleSortView'),
        ListViewBase = require('mod/listView/base/listViewBase'),
        Class = require('mod/class');
 
    var DEFAULTS = $.extend({}, ListViewBase.DEFAULTS, {
        //列表容器的选择器
        dataListSelector: '.data_list',
        //分页组件选择器
        pageViewSelector: '.page_view',
        //排序组件选择器
        sortViewSelector: '.sort_view'
    });
 
    var SimpleListView = Class({
        instanceMembers: {
            initMiddle: function () {
                var opts = this.options,
                    $element = this.$element;
 
                //缓存核心的jq对象
                this.$data_list = $element.find(opts.dataListSelector);
            },
            createPageView: function () {
                var pageView,
                    opts = this.options;
 
                if (opts.pageView) {
                    //初始化分页组件
                    delete opts.pageView.onChange;
                    this.$element.append(SimplePageView.create());
                    pageView = new SimplePageView(this.$element.find(opts.pageViewSelector), opts.pageView);
                }
                return pageView;
            },
            createSortView: function () {
                var sortView,
                    opts = this.options;
 
                if (opts.sortView) {
                    //初始化分页组件
                    delete opts.sortView.onChange;
                    sortView = new SimpleSortView(this.$element.find(opts.sortViewSelector), opts.sortView);
                }
                return sortView;
            },
            createTplEngine: function () {
                return new MustacheTpl(this.options.tpl);
            },
            querySuccess: function (html, args) {
                this.$data_list.html(html);
            }
        },
        extend: ListViewBase,
        staticMembers: {
            DEFAULTS: DEFAULTS,
            dataAttr: 'simpleList'
        }
    });
 
    return SimpleListView;
});

忽略掉SimplePageView以及SimpleSortView的实现,这个我下一篇博客会补充说明,你会发现实现一个简单的列表组件已经非常简洁了,代码不到70行。

下一篇博客补充对排序跟分页组件的说明。

1
收藏

评论





评论

0条评论

    发表评论

    电子邮件地址不会被公开。