这是我写的关于列表组件的第2篇博客。前面的相关文章有:

1. 列表组件抽象(1)-概述

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

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

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

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

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

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


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

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的返回可能是类似这样的:

{
"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的时候用的是下面类似的结构:

['{{#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:

renderParse: function(paredRows){
return {
records: paredRows
}
},

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

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

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

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

1)init方法

源码:

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方法

返回列表的参数:

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这个全局变量拿到列表组件实例。

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

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

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

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

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

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

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

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:

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

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

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函数。

//更新查询条件
//如果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为例:

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行。

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

列表组件抽象(2)-listViewBase说明的更多相关文章

  1. 【音乐App】—— Vue-music 项目学习笔记:歌曲列表组件开发

    前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记. 项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star. 当前歌曲播放列表 添加歌曲 ...

  2. Vue slot 插槽用法:自定义列表组件

    Vue 框架的插槽(slot)功能相对于常用的 v-for, v-if 等指令使用频率少得多,但在实现可复用的自定义组件时十分有用.例如,如果经常使用前端组件库的话,就会经常看到类似的用法: < ...

  3. 可展开的列表组件——ExpandableListView深入解析

    可展开的列表组件--ExpandableListView深入解析 一.知识点 1.ExpandableListView常用XML属性 2.ExpandableListView继承BaseExpanda ...

  4. Bootstrap学习之路(3)---列表组件

    列表是几乎所有网站都会用到的一个组件,正好bootstrap也给我们提供了这个组件的样式,下面我给大家简单介绍一下bootstrap中的列表组件的用法! 首先,重提一下引用bootstrap的核心文件 ...

  5. Android(java)学习笔记186:对ListView等列表组件中数据进行增、删、改操作

    1.ListView介绍 解决大量的相似的数据显示问题 采用了MVC模式: M: model (数据模型) V:  view  (显示的视图) C: controller 控制器 入门案例: acit ...

  6. bootstrap 之 列表组件使用

    列表是几乎所有网站都会用到的一个组件,正好bootstrap也给我们提供了这个组件的样式,下面我给大家简单介绍一下bootstrap中的列表组件的用法! 首先,重提一下引用bootstrap的核心文件 ...

  7. React-Native新列表组件FlatList和SectionList学习 | | 联动列表实现

    React-Native在0.43推出了两款新的列表组件:FlatList(高性能的简单列表组件)和SectionList(高性能的分组列表组件). 从官方上它们都支持常用的以下功能: 完全跨平台. ...

  8. Vue列表组件与弹窗组件示例

    列表组件 <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <me ...

  9. Android精通:View与ViewGroup,LinearLayout线性布局,RelativeLayout相对布局,ListView列表组件

    UI的描述 对于Android应用程序中,所有用户界面元素都是由View和ViewGroup对象构建的.View是绘制在屏幕上能与用户进行交互的一个对象.而对于ViewGroup来说,则是一个用于存放 ...

随机推荐

  1. 了解PHP中的Array数组和foreach

    1. 了解数组 PHP 中的数组实际上是一个有序映射.映射是一种把 values 关联到 keys 的类型.详细的解释可参见:PHP.net中的Array数组    . 2.例子:一般的数组 这里,我 ...

  2. spring源码分析之<context:property-placeholder/>和<property-override/>

    在一个spring xml配置文件中,NamespaceHandler是DefaultBeanDefinitionDocumentReader用来处理自定义命名空间的基础接口.其层次结构如下: < ...

  3. 自己来实现一个简易的OCR

    来做个简易的字符识别 ,既然是简易的 那么我们就不能用任何的第三方库 .啥谷歌的 tesseract-ocr, opencv 之类的 那些玩意是叼 至少图像处理 机器视觉这类课题对我这种高中没毕业的人 ...

  4. Android—基于GifView显示gif动态图片

    android中显示gif动态图片用到了开源框架GifView 1.拷GifView.jar到自己的项目中. 2.将自己的gif图片拷贝到drawable文件夹 3.在xml文件中设置基本属性: &l ...

  5. 萌新笔记——linux下查看内存的使用情况

    windows上有各种软件可以进行"一键加速"之类的操作,释放掉一些内存(虽然我暂时不知道是怎么办到的,有待后续学习).而任务管理器也可以很方便地查看各进程使用的内存情况,如下图: ...

  6. SpringMVC初始化参数绑定--日期格式

    一.初始化参数绑定[一种日期格式] 配置步骤: ①:在applicationcontext.xml中只需要配置一个包扫描器即可 <!-- 包扫描器 --> <context:comp ...

  7. OpenWrt中开启usb存储和samba服务

    在从官网安装的WNDR3800 15.05.1版本OpenWrt中, 不带usb存储支持以及samba, 需要另外安装 1. 启用usb支持 USB Basic Support https://wik ...

  8. Jexus Web Server 完全傻瓜化图文配置教程(基于Ubuntu 12.04.3 64位)[内含Hyper-v 2012虚拟机镜像下载地址]

    1. 前言 近日有感许多新朋友想尝试使用Jexus,不过绝大多数都困惑徘徊在Linux如何安装啊,如何编译Mono啊,如何配置Jexus啊...等等基础问题,于是昨日向宇内流云兄提议,不如搞几个配置好 ...

  9. AutoMapper(一)

    返回总目录 映射前后操作 偶尔有时候,在映射发生之前或之后,你可能需要执行一些自定义的逻辑.这可能是很少见的事情,因为在AutoMapper之外处理这些事情是更明显的.你可以创建一个映射前后的全局操作 ...

  10. 借助GitHub托管你的项目代码

    PS:话说自己注册了GitHub都很久了,却没有怎么去弄,现在系统学习一下,也把自己的学习经历总结下来share给大家,希望大家都能把GitHub用起来,把你的项目代码happy地托管起来! 一.基本 ...