在我的应用中,有一块消息处理的功能,它可以按组给相应的人发送消息。为了方便使用,増加了自定义分组的功能,用户可以自行将人员分为不同的组。目前分组只支持一级,对于日常使用目前是足够了。为了方便用户操作,因此分组的所有功能设计为单页面的操作,包括:分组的创建、改名、删除,分组的展示,切換,分组用户的添加,删除,人数的动态显示。因为涉及的功能比较多,因此采用了avalon这一mvvm的框架。前端展示主要是使用bootstrap加上自已写的一些css。与后端交互使用jquery,还用到了bootstrap的对话框插件。本文主要是介绍前端的实现,后端因为是使用uliweb框架所以不会做太多涉及。
本文不是一个avalon的教程,所以很多细节可能只是一带而过,在这里只是想介绍一下我用Avalon都做了些什么,用到了什么,以及我的一些想法。所以有些内容请参照avalon的文档来阅读。
下面让我们来一步步看分组功能的实现。
因为我使用的比较简单,所以只需要引入 avalon.js
即可。从avalon的网站下载源码包,将avalon.js拷贝到你的应用的静态文件目录下,象我是放在 /static
目录下,因此在 <head>
处添加:
<script src="/static/avalon.js"></script>
然后,找一处可以添加 javascript 的地方,比如 </body>
之前,写入:
avalon.config({
interpolate: ["{%", "%}"]
});
var model = avalon.define('Groups', function(vm){
vm.groups = {{=json_dumps(groups)}};
vm.items = [];
vm.cur_id = 0;
});
请注意,上面的代码是在uliweb框架下运行的(其实和本文还是有些差异),所以有些地方你要根据实际情况进行调整。上面的代码我解释一下:
avalon.config
是用来进行配置的。这里 interpolate
是可以更換 avalon 插值表达式的标签字符串的,原来它是 {{}}
,但是因为uliweb的模板使用 {{}}
来解析,所以和avalon的冲突了,因此通过avalon.config进行重设。如果你没有这个问题,这个设置可以不用管它。avalon.define('Groups', function(vm){})
定义了一个avalon的controller的处理函数。它对应的controller的名字是 Groups
, function中的vm参数是对应底层的viewmodel的实例,用它来处理controller中的变量。它的返回值保存到一个变量中,这里为 model
,这样可以在controller函数之外使用。{{=json_dumps(groups)}}
这是干了什么。 vm.groups
是用来存放当前有哪些组的变量。在启动这个页面时,我们需要对groups进行初始化。其实初始化有两种方式,一种是通过ajax请求从后台获取,这样可以与后台的实现无关。另一种是在模板中直接生成。这里我采用的是第二种,因此,在后台显示这个页面时,已经准备好了groups的数据,通过uliweb的一个方法将数据转为json格式。这样做也是因为每个人的组比较少,直接在模板中生成可以减少一次请求,你可以试着改为用ajax从后台获取。vm.items
用来保存当前组的人员信息。vm.cur_id
用来保存当前组的id。我利用了 items, cur_id来保存当前组的信息,意味着,在任意时刻,我只维护一组的用户信息。但是组的信息是全部的。因此,在切換组的时候,会向后台请求待切換组的信息,然后items和cur_id会发生变化。这样也是为了减化数据结构。一个完整的group信息可以设计为:
group = [{id:xx, name:yyy, items:[...]}, {id:xx, name:yyy, items:[...]}
不过这样层次比较多。因此我把items提出来,并且随着组的切換与后台进行数据的获取。
我是使用bootstrap作为ui,以下是框架代码:
<div class="container-fluid" ms-controller="Groups">
<div class="row-fluid">
<div style="margin-bottom:10px; padding-bottom:5px; border-bottom:1px solid #999;">
<a href="#" class="btn btn-primary" ms-click="add_group">添加新分组</a>
<a href="#" class="btn btn-info" ms-click="edit_group(cur_id)">修改分组名</a>
<a href="#" class="btn btn-info" ms-click="del_group(cur_id)">删除分组</a>
</div>
<div id="tabs" class="tabbable tabs-left rounded" style="background-color:white;padding:10px;">
<ul class="nav nav-tabs" ms-each-tab="groups">
<li>
<a href="#items" data-toggle="tab" ms-click="active_tab(tab.id)">{% tab.name %}</a>
</li>
</ul>
<div class="tab-content" ms-if="groups.size()">
<div class="tab-pane" id="items">
<div>
<a href="#" class="btn btn-primary btn-small" ms-click="add_user(cur_id)">添加人员</a>
总人数: {%items.size()%}
</div>
<ul ms-each-user="items" class="unstyled group-users clearfix">
<li>
<div class="body" ms-hover="show">
<img ms-src="user.image"/>
{% user.url|html %}
<span class="delete" ms-click="del_user(user.id, cur_id)">×</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
说明一下:
最外层是一个div,它有一个非常关键的属性 ms-controller="Groups"
表明它是一个avalon的controller。
整体结构就是bootstrap的tab结构,就不作过多解释了。通过 <div id="tabs" class="tabbable tabs-left">
在class上添加tabs-left让tab显示在左侧,主要是让组太多的话,可以竖着排列。
在tabs区上面是若干的按钮,通过 ms-click
来绑定处理函数,如: add_group
, edit_group(cur_id)
。其中 add_group
没有括号,当没有参数时可以简化成这样。这样需要在define中添加一个 add_group
的函数。 edit_group(cur_id)
因为有参数所以是标准的函数的写法,在define中需要添加一个带参数的处理函数。cur_id
是对应 vm.cur_id
变量,表示当前的组id。
在tabs区的ul中,添加了 ms-each-tab="groups"
,它用来控制下面子元素的循环。它是用来显示group的tab的。这里 tab
是循环变量,groups
对应 vm.groups
变量。在循环体中将生成若干的 <a>
标签 。在 <a>
标签中添加了 ms-click="active_tab(tab.id)"
,这样就绑定了click事件到 active_tab
方法上,同时传入 tab.id
这个变量。<a>
的值是 {% tab.name %}
。这是avalon中的插件表达式的写法。还记得吗?我们在前面将 {{}}
換成了 {%%}
。所以它的显示是 tab.name 的值。
因为我的tab对应的内容是动态生成的,所以我只定义了一个 <div class="tab-content" ms-if="groups.size()">
。这里使用了 ms-if 属性,表示只有当 groups.size()
为真,即个数大于 0时才会显示出来。因此,如果groups在开始为空,这个div是不会出现的。
在每个tab的内容部分,先是有一个按钮,绑定了 add_user(cur_id)
事件,用来向指定的group中添加用户。然后在后面通过 {%items.size()%}
来显示items的个数,即当前组的总人数。这里 size()
是 avalon 为监控数组添加的一个方法,它可以随着内容条数的变化,对页面的引用进行修改。而使用 items.length
则起不到这个作用。所以这一点要注意。
下面就是显示每个组的人员信息了,所以使用了 ul>li 的结构。在ul上添加了一个ms-each的循环: ms-each-user="items"
。这样对 items
进行循环,循环变量是 user
。
在循环体中,添加了 ms-hover="show"
作用是当鼠标在元素上移动时,向元素添加一个class,即 show
。这样我们可以通过定义相对应的css来处理有show和无show时的样式,来显示当前鼠标所指向的元素的效果。
每个用户将显示一个头像,姓名和删除按钮,分别用:
<img ms-src="user.image"/>
{% user.url|html %}
<span class="delete" ms-click="del_user(user.id, cur_id)">×</span>
来处理。 ms-src
用来引用 user
变量中的 image
属性值。 {% user.url|html %}
用来显示用户名,它其实是一个可点击的链接,所以是url。而 |html
是Avalon中定义的过滤器(在avalon中定义了一些常用的过滤器,比如对日期格式的转換等),它可以保持内容不被转义,即保留文本中的html特殊符号。如果不加,则自动转义。通过span来生成一个删除按钮,将click事件与删除用户函数相绑定,它的参数是 user.id
表示当前用户的id,和 cur_id
表示当前组。对于删除按钮,缺省的样式是不显示的。当父元素添加了 show
之后,就会显示出来。所以它的显示是通过css来控制的。
以上就是ui的详细介绍。从而我们可以了解avalon(mvvm框架)的一些特点:
通过以上的绑定,数据和展示非常好的结合在了一些。那么,当数据发生变化,会使得界面自动进行改变。所以一旦界面绑定完毕,我们下面就只要处理数据是如何变化的就可以了。
其实Avalon的主要难点在界面,数据处理我只是举几个例子。
先看一下添加组的示例:
vm.add_group = function(){
var name = prompt("请输入新的收信人分组名称:");
if (name){
$.post('/config/messages/add_group', {'name':name})
.success(function(data){
if (data.success){
show_message(data.message);
vm.groups.push(data.data);
setTimeout(function(){
vm.active_tab(data.data.id);
$('#tabs ul a:last').tab('show');
}, 100);
}else{
show_message(data.message, 'error');
}
});
}
}
$.post
来与后台进行通讯。这里, $.post
是jquery的方法,因此我的这个示例其实还需要安装jquery,如何引入jquery我就不再说了。我发现avalon和jquery可以很好的结合,至少从我目前的使用来说是这样的。而angularjs则在这一点上要麻烦一些。avalon中,通过其它的非Avalon的方法可以直接修改vm中的数据,并会影响界面的变化;而angularjs使用jquery的方法修改了model的数据,界面不会直接变化,还要执行如$scope.apply()之类来强制刷新的方法或者使用Angularjs自带的ajax方法。所以我个人感觉在与jquery的结合上,avalon要优于angularjs。{success:boolean, message:消息, data:数据}
。因此data参数其实是这种结构,所以data.data是返回的数据。它其实是一个object,格式为 {id:xxx, name:yyy}
。active_tab
来切換数据,然后调用bootstrap的方法来切換tab。其实,延迟执行主要是因为在一系列的串行处理中,夹杂了异步处理(自动刷新)导制,而这个异步处理目前没有办法以得到它的一些状态。先看代码:
vm.active_tab = function(id){
vm.cur_id = 0;
vm.items = [];
if(id){
vm.cur_id = id;
}else{
if (vm.groups.length>0){
vm.cur_id = vm.groups[0].id;
}
}
if(vm.cur_id>0){
$.get('/config/messages/get_group/'+vm.cur_id)
.success(function(data, status, headers, config){
vm.items = data;
orderBy(vm.items, 'email');
});
}
}
cur_id
和 items
进行初始化。主要是为了保证旧的数组不会遗留。对于象数组这样的结构,我曾经发现,如果不清空直接修改的话,有可能以前的数据会遗留下来,这个是avalon为了效率这样的处理。所以清空就保证了没有遗留。不知道这点以后有可能可能变化。这样反正也没有错。cur_id
。如果没有给出id参数,则自动取vm.groups中的第一个id。cur_id > 0
,则通过 $.post()
与后台通讯,获得当前group的所有用户,这里没有采用 {success:boolean, message:消息, data:数据}
的格式,而是直接返回一个数组。所以是 vm.items = data
。得到数据后,对它进行了一个排序,根据 邮件地址 。这里 orderBy是我单独写的一个方法, 不是avalon提供的。vm.edit_group = function(group_id){
var name = prompt("请输入新的收信人分组名称:");
if (name){
$.post('/config/messages/edit_group/'+group_id, {'name':name})
.success(function(data){
if (data.success){
for(var i=0;i<vm.groups.length;i++){
if(vm.groups[i].id === group_id)
vm.groups[i]['name'] = name;
}
}else{
show_message(data.message, 'error');
}
});
}
}
这里主要说一下调用后台的处理成功后,如何修改vm.groups的数据。对vm.groups进行循环,比较group的id是否等于修改的组的id,即上面的 if(vm.groups[i].id === group_id)
,如果相等,则替換 name : vm.groups[i]['name'] = name
。这样当数据一变,界面自然跟着变化。
vm.del_group = function(group_id){
var ret = confirm("你确定要删除此分组吗?");
if (ret){
$.post('/config/messages/del_group', {'id':group_id})
.success(function(data){
if(data.success){
show_message(data.message);
var i=0;
for(;i<vm.groups.length;i++){
if(group_id === vm.groups[i].id){
vm.groups.splice(i, 1);
break;
}
}
vm.active_tab();
$('#tabs ul a:first').tab('show');
}
else{
show_message(data.message, 'error');
}
});
}
}
最主要的就是如何修改数据,就是上面的 vm.groups.splice(i, 1);
,将删除数组中找到的索引下标的值。
通过avalon可以大大减化前端界面的开发,基本上我们只要考虑:如何展示界面,如何处理数据。当数据变化时,Avalon为你搞定一切。当前目前还是有些理想。而且我上面的例子还是非常简单,许多avalon的功能都没有用到,比如include, router等。使用Avalon之后,大量琐碎的处理都没有了,比如:添加,修改,删除之后对DOM元素的处理,当人员变化时对DOM元素的处理,以及人员总数的处理。如果只是简单使用jquery,除了后台通讯的处理,我们还需要大量的代码来处理这些细节。当然,我们也可以封装出若干当数据变化时更新界面的函数,但是没有直接使用Avalon这样顺畅。avalon目前还在快速发展中,还可能存在不足,所以要在使用中比较仔细,了解它能做什么,不能做什么,怎么作是正确的,这样才能比较好的使用它。随着它的不断完善,我想它会越来越好用。