使用avalon实现用户分组管理的介绍

在我的应用中,有一块消息处理的功能,它可以按组给相应的人发送消息。为了方便使用,増加了自定义分组的功能,用户可以自行将人员分为不同的组。目前分组只支持一级,对于日常使用目前是足够了。为了方便用户操作,因此分组的所有功能设计为单页面的操作,包括:分组的创建、改名、删除,分组的展示,切換,分组用户的添加,删除,人数的动态显示。因为涉及的功能比较多,因此采用了avalon这一mvvm的框架。前端展示主要是使用bootstrap加上自已写的一些css。与后端交互使用jquery,还用到了bootstrap的对话框插件。本文主要是介绍前端的实现,后端因为是使用uliweb框架所以不会做太多涉及。

本文不是一个avalon的教程,所以很多细节可能只是一带而过,在这里只是想介绍一下我用Avalon都做了些什么,用到了什么,以及我的一些想法。所以有些内容请参照avalon的文档来阅读。

下面让我们来一步步看分组功能的实现。

整体布局

在此输入图片描述

  • 最上面是几个按钮,用来控制组的添加、组名修改和删除。
  • 下面左侧是分组的tab,可以点击进行切換。
  • 每个分组会显示一个添加按钮,当前总人员和目前所有人。每个人显示一个头像,人名。当鼠标移上去,会显示一个框和一个删除的图标,点击后可以删除此人。

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框架下运行的(其实和本文还是有些差异),所以有些地方你要根据实际情况进行调整。上面的代码我解释一下:

  1. avalon.config 是用来进行配置的。这里 interpolate 是可以更換 avalon 插值表达式的标签字符串的,原来它是 {{}} ,但是因为uliweb的模板使用 {{}} 来解析,所以和avalon的冲突了,因此通过avalon.config进行重设。如果你没有这个问题,这个设置可以不用管它。
  2. avalon.define('Groups', function(vm){}) 定义了一个avalon的controller的处理函数。它对应的controller的名字是 Groups, function中的vm参数是对应底层的viewmodel的实例,用它来处理controller中的变量。它的返回值保存到一个变量中,这里为 model,这样可以在controller函数之外使用。
  3. {{=json_dumps(groups)}} 这是干了什么。 vm.groups 是用来存放当前有哪些组的变量。在启动这个页面时,我们需要对groups进行初始化。其实初始化有两种方式,一种是通过ajax请求从后台获取,这样可以与后台的实现无关。另一种是在模板中直接生成。这里我采用的是第二种,因此,在后台显示这个页面时,已经准备好了groups的数据,通过uliweb的一个方法将数据转为json格式。这样做也是因为每个人的组比较少,直接在模板中生成可以减少一次请求,你可以试着改为用ajax从后台获取。
  4. vm.items 用来保存当前组的人员信息。
  5. 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>

说明一下:

  1. 最外层是一个div,它有一个非常关键的属性 ms-controller="Groups" 表明它是一个avalon的controller。

  2. 整体结构就是bootstrap的tab结构,就不作过多解释了。通过 <div id="tabs" class="tabbable tabs-left"> 在class上添加tabs-left让tab显示在左侧,主要是让组太多的话,可以竖着排列。

  3. 在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。

  4. 在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 的值。

  5. 因为我的tab对应的内容是动态生成的,所以我只定义了一个 <div class="tab-content" ms-if="groups.size()"> 。这里使用了 ms-if 属性,表示只有当 groups.size() 为真,即个数大于 0时才会显示出来。因此,如果groups在开始为空,这个div是不会出现的。

  6. 在每个tab的内容部分,先是有一个按钮,绑定了 add_user(cur_id) 事件,用来向指定的group中添加用户。然后在后面通过 {%items.size()%} 来显示items的个数,即当前组的总人数。这里 size() 是 avalon 为监控数组添加的一个方法,它可以随着内容条数的变化,对页面的引用进行修改。而使用 items.length 则起不到这个作用。所以这一点要注意。

  7. 下面就是显示每个组的人员信息了,所以使用了 ul>li 的结构。在ul上添加了一个ms-each的循环: ms-each-user="items" 。这样对 items 进行循环,循环变量是 user

  8. 在循环体中,添加了 ms-hover="show" 作用是当鼠标在元素上移动时,向元素添加一个class,即 show 。这样我们可以通过定义相对应的css来处理有show和无show时的样式,来显示当前鼠标所指向的元素的效果。

  9. 每个用户将显示一个头像,姓名和删除按钮,分别用:

    <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框架)的一些特点:

  • 提供了数据模型与DOM页面结合的方法
  • 提供了UI的一些展示效果,如我用到的: ms-if, ms-hover, ms-src等
  • 提供了数据与事件的绑定
  • 提供了基本的处理逻辑,如 ms-each-xxx

通过以上的绑定,数据和展示非常好的结合在了一些。那么,当数据发生变化,会使得界面自动进行改变。所以一旦界面绑定完毕,我们下面就只要处理数据是如何变化的就可以了。

数据处理

其实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');
                }
            });
    }
}
  1. 先提示用户输入组名。
  2. 然后如果输入了值,则调用 $.post 来与后台进行通讯。这里, $.post 是jquery的方法,因此我的这个示例其实还需要安装jquery,如何引入jquery我就不再说了。我发现avalon和jquery可以很好的结合,至少从我目前的使用来说是这样的。而angularjs则在这一点上要麻烦一些。avalon中,通过其它的非Avalon的方法可以直接修改vm中的数据,并会影响界面的变化;而angularjs使用jquery的方法修改了model的数据,界面不会直接变化,还要执行如$scope.apply()之类来强制刷新的方法或者使用Angularjs自带的ajax方法。所以我个人感觉在与jquery的结合上,avalon要优于angularjs。
  3. 如果成功,会调用 show_message 来显示成功的信息。这里show_message是我自已写的一个方法和avalon无关。成功后,会将返回的数据添加到vm.groups的最后。在我的应用里我定义前后段的json消息格式为: {success:boolean, message:消息, data:数据} 。因此data参数其实是这种结构,所以data.data是返回的数据。它其实是一个object,格式为 {id:xxx, name:yyy}
  4. 然后使用了setTimeout 来延迟更新tab切換。可能你要问?为什么不在push新的group之后就调用切換tab的处理呢?这是因为,数据变化之后,到界面刷新还是需要一定的时间的。avalon采用监控属性变化的方式来异步更新,所以更新数据之后马上执行,有可能早于界面的刷新,造成切換无效。采用延迟方式来刷新,是希望界面已经变化之后再执行,避免错误。那么能不能做到更精确的时间切換呢?目前还很困难,因为异步的原因,无法得到准确的时机。所以象angularjs也存在同样的问题。对于我来说,我是通过$scope.apply()来强制刷新,而Avalonjs中好象没有这样的用法。所以采用了延迟执行。100毫秒对于我这个简单的应用来说足够了。不过如果有更好的方式希望能告诉我。在延迟执行中,先调用了 active_tab 来切換数据,然后调用bootstrap的方法来切換tab。其实,延迟执行主要是因为在一系列的串行处理中,夹杂了异步处理(自动刷新)导制,而这个异步处理目前没有办法以得到它的一些状态。

切換当前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');
            });
    }
}
  1. 先对 cur_iditems 进行初始化。主要是为了保证旧的数组不会遗留。对于象数组这样的结构,我曾经发现,如果不清空直接修改的话,有可能以前的数据会遗留下来,这个是avalon为了效率这样的处理。所以清空就保证了没有遗留。不知道这点以后有可能可能变化。这样反正也没有错。
  2. 然后修改 cur_id 。如果没有给出id参数,则自动取vm.groups中的第一个id。
  3. 如果 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目前还在快速发展中,还可能存在不足,所以要在使用中比较仔细,了解它能做什么,不能做什么,怎么作是正确的,这样才能比较好的使用它。随着它的不断完善,我想它会越来越好用。

你可能感兴趣的:(使用avalon实现用户分组管理的介绍)