最近几天开始看backbone.js,backbone.js是什么就不做介绍了,是一个MVC的框架,怎么用的话,有一个人写的书写的相当不错,推荐看下《Backbone.js入门教程》,正好自己现在做的项目是一个后台的管理系统,比较适合使用这种MVC的框架,前段时间,自己还尝试过使用angular.js来实现过一次一个页面,这个以后有机会再写一下这个实现的思路。这个先不提,这次正好借这个结果练习一下backbone.js的使用。
这种MVC的框架我觉得比较适合单页应用的开发,其实我们这个页面除了还会有页面的跳转外(这一点导致了不会用到框架中的路由功能),还是很像一个单页应用的,好了,废话不多说,先上一下最后页面的效果。
图1 产品效果图
图2 产品模块划分
简单分析一下这个页面,上部是一个查询的panel,包含了很多的查询条件,中间是一个表格,用来展示视频的信息,在表格里面有一列是专门的操作属性,会有很多操作。下部是一个近似的表格整体的操作panel,包含一些全选、业务操作以及分页,其实分页从逻辑上讲放到表格的这一部分是最好的。
我们先来看一下页面里面会有几个model和view。
首先,查询panel这一块会有一个model,最开始我在想这一块有没有必要单独抽出一个model,因为这个model可能不会有什么默认值,也不会有什么数据处理逻辑,但是后面我想到整个这一块初始化的时候会提取出默认的查询数据对象,这个时候肯定就需要一个数据承载的容器,可以用一个内存的变量,页可以用一个model,这里我选择使用一个model,
暂定这一块model叫做QueryModel,定义的代码如下:
var QueryModel = Backbone.Model.extend({
});//查询model,暂时是一个空model
var queryModel = new QueryModel();//定义一个单例的model对象
//对应查询panel还需要一个view对应,这个view就做两件事,第一,初始化的时候装配model,第二,单击查询之后,告诉主模块产生了查询,主模块再进行相应的处理。
var QueryView = Backbone.View.extend({
el:$("#js-queryContainer"),//查询组件的容器
events:{
"click *[data-query=btn]":"queryHandle"
},
initialize:function(){
var that = this;
function dataHandle(list,key){//数据处理
var attrKey = "data-"+key,
json = {},
attr, value;
for(var i = 0, len = list.length; i
这一块的dom结构我需要说明一下,每一个查询的条件都定义了data-query属性,这个属性的值就是要查询的条件的key,比如说data-query=”name”,就代表了这一项条件是name,值就是这个input的值。具体的结构如下:
视频ID:
视频名称:
状态:
属性:
以上是查询panel的部分,接下来是表格,我们先定义model,首先每一行的有一个数据模型,这个模型的value是我们异步获取出来的,我们还要给这个model一个选中的默认值,和默认的索引(方便后续的处理),同时提供一个toggle选中状态的方法。具体代码如下:
var GridItem = Backbone.Model.extend({//视频模型
defaults:function(){
return{
checked:false,
index:gridItems.getNextIndex()//设置索引值,girdItems 是GridItems的实例
}
}
toggleCheck:function(){
this.set({checked: !this.get("checked")});//切换选中状态
}
});
然后要给定一个GridItem的集合,GridItems目前考虑提供一个获取选中以及未选中griditem的集合的方法,以及一个用于GridItem产生默认索引值的方法,具体代码如下:
var GridItems = Backbone.Collection.extend({
model:GridItem,
getChecked:function(bool){//获取选中或者未选中的gridItem的集合
return this.where({checked: bool});
},
getNextIndex:function(){
if(!this.length){
return 0;
}
return this.last().get("index") + 1;
}
});
var gridItems = new GridItems();
数据定义完之后是对应的视图,显然这个地方会至少存在两个视图,一个是每一行的数据条,对应的model是gridItem,一个是整个的数据表格,对应的model是gridItems。每一行的视图我们暂时这么定义:
var GridItemView = Backbone.View.extend({
tagName:"tr",
events:{
"click .js-videoSelect":"onSelect",//单选框
"click .js-videoOperate":"onOperate"//操作盘
},
initialize:function(){
this.listenTo(this.model, "change", this.render);//数据改变重新渲染,数据视图分离
this.listenTo(this.model, "destroy", this.remove);//数据移除,先移除了数据之后才会触发这个处理,remove是内置的方法,擦除视图
},
template:_.template(BackboneTpl.VideoList),
render:function(){
this.$el.html(this.template(this.model.toJSON()));//渲染结果,这里要注意,要把model转换成真正的数据对象,model本身包含了数据逻辑,不能用于渲染
return this;
},
clear:function(){
this.model.destroy();//销毁数据,销毁数据之后会擦除本条视图
},
onSelect:function(event){
this.model.toggleCheck();
},
onOperate:function(event){
//todo
}
});
这里面,我定义了一个全局的模板对象BackboneTpl,用来存放模板,我们可以看一下每一条数据的结构。
图3 单行数据的关键点
我们注意到每一行的视图除了展示数据之外会存在两个操作,一个是单选框的点击,一个是操作区域的点击,在我的代码里面有专门的针对这两部分绑定事件,其中,操作区域采用的是事件代理的方式,减少事件绑定。
接下来是表格整体的视图,大概想一下,在表格里面,我们需要把gridItems的每一条数据跟gridItemView绑定起来,同时生成的dom我们要加到表格的容器里面。具体的代码如下:
var GridView = Backbone.View.extend({
el:$("#js-gridContainer"),
initialize:function(){
var queryView = new QueryView();
this.selectAllEle = $("#js-selectAll");
this.selectAllEle.click($.proxy(this.toggleAll, this));//全选
this.listenTo(gridItems, "add", this.addOne);
this.listenTo(gridItems, "reset", this.addAll);
getListData($.proxy(this.getListDataSuccess, this), $.proxy(this.getListDataError, this));
},
getListDataSuccess:function(data){//获取数据成功的回调
if(data["status"] == "0"){
var list = data["list"];
gridItems.reset(list);
}
},
getListDataError:function(){//获取数据失败的回调
//error
},
addOne:function(model){
var gridItemView = new GridItemView({model:model});
this.$el.append(gridItemView.render().el);
},
addAll:function(){
gridItems.each(this.addOne, this);
},
toggleAll:function(){//公布的对外的接口
var bool = this.selectAllEle.get(0).checked;
gridItems.each(function(item){
item.set({checked:bool})
});
}
});
Backbone的Collection可以通过fecth()方法来获取初始的数据,返回的数据会直接装配到对应的model里面,但需要后端的配合,我们的项目本身后端接口不是这么定义的,因此我们手动获取数据,手动装配。getListData是提前定义的获取数据的方法,获取数据成功之后,我调用了gridItems的reset方法,重置了gridItems里面的数据,之后会绑定相应的itemView,渲染出对应的视图。写到这一步,初始化列表展示大概就可以实现了。
列表实现了之后,我们再把分页加进去,分页跟列表应该是相互独立的部分,他们之间会通过消息来相互影响,列表的部分会往外派发一个切页的事件,表格对切页来做响应(实际上,最后会有一个专门的外部逻辑来处理组合所有的模块,表格从理论上来说应该只是做一个展示,而不负责数据的获取)。我们首先定义分页的数据模型,只需要给定默认值就可以了。
var PageModel = Backbone.Model.extend({
defaults:{
curPage:1,
pageSum:1,
itemSum:0
}
});
对应的视图: var PageView = Backbone.View.extend({
el:$("#js-page"),
events:{
"click .js-lastPage":"onTolast",
"click .js-nextPage":"onTonext",
"blur .js-current":"onCurrentblur"
},
template:_template(BackboneTpl.Page),
initialize:function(){
this.listenTo(this.model, "change", this.render);
},
render:function(){
this.$el.html(this.template(this.model.toJSON()));
},
onTolast:function(){
var cur = this.model.get("curPage"),
sum = this.model.get("pageSum");
if(cur >1){
cur--;
}
this.toXPage(cur);
},
onTonext:function(){
var cur = this.model.get("curPage"),
sum = this.model.get("pageSum");
if(cur sum){
val = sum;
}
else{
val = val;
}
}
this.toXPage(val);
},
toXPage:function(x){
this.model.set({curPage:x});
this.trigger("page");//触发了切页的事件
}
});
我们现在要对GridView进行修改,首先增加两个成员变量(只展示修改的部分)
var GridView = Backbone.View.extend({
.....,
initialize:function(){
.......;
this.selectAllEle = $("#js-selectAll");
this.pageModel = new PageModel();
this.pageView = new PageView({model:this.pageModel});
this.enterDocument();
this.getData();
},
.....
};
然后再收到数据之后更新this.pageModel。
.......,
getListDataSuccess:function(data){//获取数据成功的回调
if(data["status"] == "0"){
........;
gridItems.reset(list);
this.pageModel.set({//分页的模块更新
pageSum:parseInt(data["totalpage"]),
itemSum:parseInt(data["total"])
});
}
},
......
此外,我们将事件绑定的部分抽成一个函数enterDocument,在里面加入对切页的绑定。
......,
enterDocument:function(){
selectAllEle.click($.proxy(this.toggleAll, this, selectAllEle));
this.listenTo(gridItems, "add", this.addOne);
this.listenTo(gridItems, "reset", this.addAll);
this.listenTo(this.pageView, "page", this.onPagechange);
this.listenTo(queryView, "query", this.getData);
},
onPagechange:function(){
var curPage = this.pageModel.get("curPage");
if(curPage != this.curPage){
this.curPage = curPage;
queryModel.set({pagenum:curPage-1});
this.getData();//获取数据抽成单独函数
}
}
.......,
测试发现,切页之后原有的数据视图没有擦除,新的视图加到了原有的视图下面,应该是Collectio的reset方法并不会destory原来的数据,因此,我们在获取数据之后应该先清一下原来的数据。所以我们在getListDataSuccess函数里面加一句:
_.invoke(gridItems.slice(0), "destroy");//销毁原来的数据
销毁了原数据之后,再调用reset方法重置数据。这里要注意的是,我原来使用的第一个参数是gridItems.models结果报错,应该是因为销毁会改变了他们在内部的索引值,导致collection找不到对应的model,于是我使用了slice克隆出了一份列表,来进行相应的操作,估计collection.where返回的应该也是一份克隆的列表。
此时,整个列表的展示部分的功能就完成的,测试的时候发现,查询panel部分的功能有点想当然了,可能是受之前angular的版本的影响,没有加入更新数据模型的逻辑(angular双向绑定,自动更新),这个时候就涉及到这一块的设计方法了,在我们的真正项目中,我并没有去维护一个数据模型,而是在产生查询的时候去查询产生一个json对象,不过既然前面这里有一个数据模型,那么我们就手动的维护一下,增加一个change事件的绑定,数据更改了之后更新一下queryModel。
var QueryView = Backbone.View.extend({
.......,
initialize:function(){
var that = this;
function dataHandle(list,key){//数据处理
.......;
};
function dataInit(){//数据处理
var queryList = that.$("*[data-query]");
dataHandle(queryList, "query");
that.$el.change($.proxy(that.dataChange, that));//增加事件监听
};
dataInit();
},
dataChange:function(event){
var target = event.target,
attr = target.getAttribute("data-query"),
val = target.value,
json = {};
json[attr] = val;
queryModel.set(json);
},
queryHandle:function(){
.......
}
});
至此,本页基本的列表展示的功能已经完成,页面中还有一些功能会在后续继续改造。