使用mvvm框架avalon开发公司内部运营管理系统的一些心得
接触avalon差不多有一年时间了,当时是看前端大牛司徒正美的博客才了解到还有这么一个高大上的玩意,然后就加入了avalon的讨论群。从群里零零散散的了解了avalon的一些特性,感觉很强大,感觉思想比较超前,连jquery也成了他们的吐槽对象。司徒正美鼓吹使用avalon代码量比使用jquery至少少一半,不用操作DOM,更易维护,完全只需要操作Model数据层,界面会自动更新,当时听到这些概念的时候我也会思考某些功能不用jquery实现转而使用avalon实现的话怎么实现,当时还是有很多疑惑,觉得使用惯了jquery突然不使用的话都不知道怎么做开发了。当时也专门看过avalon的入门教程,由于在项目中用得少,很快就忘了。
在博雅,每天的活动任务量特别多,记得刚进博雅做的第三个活动是六合彩活动,因为一般活动开发时间都是3天左右(重构+交互),时间很紧,而且自己对六合彩规则一点都不懂,所以拿到这个活动,第一步就是熟悉规则,当时觉得这个活动挺好玩,应该不难,可是越做到细节就感觉需要考虑的地方特别多,交互特别多,界面更新特别多,于是大量的dom选择和操作充斥着整个开发,当时完成这个活动的时候js代码都达到了2000行,开发完成之后让我最觉得恶心的就是大量的dom操作,做什么操作都要去选择dom,然后根据dom进行操作,代码量一大,定位代码都很麻烦,像这样写的代码估计也就只能使用一次,没有可维护性。然后就突然想到了司徒正美的avalon框架,不用操作dom,当时觉得这应该是前端程序员最幸福的事情了,现在来看,如果当初使用avalon来开发这个活动,代码量最多也就1000行。于是,在后面活动的开发中,我是一边开发一边参考avalon入门教程,感觉越用越爽,开发活动速度越来越快,由之前的3天缩短到2天甚至更快。于是,爱上了avalon,开始有点厌倦jquery了。
这次公司内部的运营管理系统因为交互多,数据交互量大,针对这样一个比较庞大的系统,如果使用jquery的话估计又会被dom折腾死,所以我毅然选择了avalon进行开发。因为现阶段基本完成了游戏侧的开发,所以想总结下使用avalon开发项目的一些心得,一来让还没用过avalon的同事了解一下avalon与jquery的区别,二来也是给自己做一个项目开发的总结。
先来看看项目结构:
这里我用的是seajs模块化管理的库,引入jquery仅仅是为了使用ajax请求,util目录下是一些工具类,widget是一些ui组件,page下为相关的业务处理逻辑,这里因为分成游戏方和平台方所以用两个文件夹加以区分,calendar.js实现的是日历功能以及在日历上进行数据展示和相关业务操作,actlists.js文件实现的是下面活动申请列表的相关展示和处理。相关界面如下:
日历功能包括切换上一个月,切换下一个月,回到今天,而且展示当前月每天的活动申请量,在今天以前禁止申请,以深灰色标识,点击单元格不会弹出申请弹框,从今天到下周周末是限制申请,以浅灰色来标识,在用户点击单元格的时候会弹出一个dialog弹框,用户点击确定才弹出活动申请弹框,点击取消关闭dialog弹框,其余的是可以申请的,点击单元格弹出活动申请弹框。
首先,为了展示这样一个日历,我定义了一个数组,将当前月,上个月的后几天,下个月的前几天都放在了这个数组里,数组里没一个元素都是一个对象,用来记录当天的信息,比如年,月,日,是否是今天,是否被限制,是否被禁止,是否是当前月,当日的时间戳,以及当日的活动申请量。看代码:
function getWeeks (ooo, appList) { var dateObj = new Date(); var todayTime = new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate()) - 0; var todayDate = dateObj.getDay(); var year = ooo.getFullYear(); var month = ooo.getMonth(); //得到今天是几月(0 ~ 11) var date = ooo.getDate(); //得到今天是几号 (1 ~ 31) var cur = new Date(year, month, date); cur.setMonth(month + 1); //改为下一个月 //由于日期是1 ~ 31, 0则是退到上一个月的最后一天,于是得到这个月的总天数 cur.setDate(0); var num = cur.getDate(); var next = 6 - cur.getDay(); var dates = avalon.range(1, num + 1); var currentDay; dates = dates.map(function(d) { return dateObj.getFullYear() === year && dateObj.getMonth() === month && date === d ? { year : year, month : month, date : d, num : appList[d], flag : true, isToday : true, time : new Date(year, month, d) - 0 } : { year : year, month : month, date : d, num : appList[d], flag : true, isToday : false, time : new Date(year, month, d) - 0 }; }); cur.setMonth(month); cur.setDate(1); //得到当月的第一天 var prev = cur.getDay(); //0 ~ 6 cur.setDate(date); //还原 for (var i = 0; i < prev; i++) {//补上上一个月的日期 var curr = new Date(year, month, -1 * i); dates.unshift({ year : year, month : month - 1, date : curr.getDate(), flag : false, isToday : false, time : curr - 0 }) } for (i = 0; i < next; i++) {//补上下一个月的日期 curr = new Date(year, month + 1, i + 1); dates.push({ year : year, month : month + 1, date : curr.getDate(), flag : false, isToday : false, time : curr - 0 }) } for (i = 0, len = dates.length; i < len; i++) { currentDay = dates[i]; if (currentDay.time >= todayTime) { if (currentDay.time - (14 - todayDate) * 24 * 60 * 60 * 1000 < todayTime) { currentDay.isLimit = true; } else { currentDay.isForbid = false; } } else { currentDay.isForbid = true; } } var ret = []; while (dates.length) {//每行七个分组 ret.push(dates.splice(0, 7)); } return ret; //一个二维数组 }
返回的数据格式如下:
获取到数据之后,只需要利用avalon的动态模版引擎直接渲染就生成了上面的日历,看模版代码:
<tr ms-repeat-el="calendar"> <td ms-class="day_style:elem.flag==true" ms-class-1="day_gray:elem.isLimit==true" ms-class-2="day_darkGray:elem.isForbid==true" valign="top" ms-repeat-elem="el" ms-click="clickApply(elem)"> <div class="day_mouse"><div class="day_no" ms-if="elem.isToday!=true">{{elem.date}}</div><div class="day_no" ms-class="today:elem.isToday==true" ms-if="elem.isToday==true">今天</div><div class="day_main"><p ms-if="elem.num>0">{{elem.num}}个资源申请</p></div></div> </td> </tr>
这个ms-repeat-el="calendar"中的calendar就是上面的二维数组,然后针对模版进行了二次循环输出,里面有大量的以ms-开头的是用于处理模版逻辑的,比如ms-class-1="day_gray:elem.isLimit==true"就是当前日对象isLimit为true时就加上day_gray类名,其他代码属性大家可以去司徒正美博客了解下哈。针对切换上一个月,下一个月,回到今天的功能我们只需要操作calendar这个数组就行,完全的操作model数据层,没有任何dom出场的机会,这样的代码维护性高,功能划分清晰,给人一种特别愉快的开发感受。
同样的,针对下面活动列表展示和过滤我们也是将所有的活动放在了一个数组里,然后针对数组进行模版渲染,看代码:
//活动列表展示及过滤 var actListObj = avalon.define('applyActLists', function (vm) { vm.actlists = []; //申请过的活动列表 vm.$actlistsClone = []; //申请过的活动列表副本 vm.status = -1; //活动过滤字段,-1为全部状态 vm.filterActListCallback = function (val) { vm.status = val; switch (val) { case '-1': actListObj.actlists = actListObj.$actlistsClone; break; case '0': tools.filterActLists(actListObj.$actlistsClone, 0); break; case '1': tools.filterActLists(actListObj.$actlistsClone, 1); break; case '2': tools.filterActLists(actListObj.$actlistsClone, 2); break; case '3': tools.filterActLists(actListObj.$actlistsClone, 3); break; } }; });
相应的模版代码:
<div class="cur_progress" ms-controller="applyActLists"> <div class="title"> <div class="left"><b>我的活动申请的进度:</b></div> <div class="right"> <span class="green_block color_block"></span><span>审核已通过</span> <span class="yellow_block color_block"></span><span>审核中</span> <span class="red_block color_block"></span><span>审核被驳回</span> <span class="white_block color_block"></span><span>待提交</span> <select id="s1_text1_bold" ms-duplex="status" data-duplex-changed="filterActListCallback"> <option value="-1" selected>全部状态</option> <option value="0">待提交</option> <option value="1">审核中</option> <option value="2">审核被驳回</option> <option value="3">审核已通过</option> </select> </div> </div> <dl> <dd ms-repeat="actlists" ms-class="odd:$index%2==1"> <div class="top"><b>{{el.actName}}</b></div> <div class="bottom"> <a href="javascript:" class="a_submit a_act" ms-class-1="a_act_yellow:el.step==1&&el.status==1" ms-class-2="a_act_red:el.step==1&&el.status==2" ms-class-3="a_act_green:el.step>1" ms-click="show_apply_detail(el)">提交审核</a><span class="g_dot"></span> <a href="javascript:" class="a_self_test a_act" ms-class-1="a_act_white:el.step==2&&el.status==0" ms-class-2="a_act_green:el.step>2">自测通报</a><span class="g_dot"></span> <a href="javascript:" class="a_scheduling a_act" ms-class-1="a_act_green:el.isScheduled==1" ms-class-2="a_act_yellow:el.step==3&&el.status==1">查看排期详情</a><span class="g_dot"></span> <a href="javascript:" class="a_effect a_act" ms-class-1="a_act_green:el.isOnline==1" ms-class-2="a_act_yellow:el.step==4&&el.status==1">查看上线效果</a> </div> </dd> </dl> </div>
活动申请的过滤功能是在select上绑定了change事件的回调函数filterActListCallback(data-duplex-changed="filterActListCallback"),当select的值发生改变时,针对select的当前值返回新的活动列表数组,然后界面就会自动更新,如下:
我们看到,利用avalon可以大大加快我们项目的开发速度,可以使我们远离dom的世界,我们的编程变成了只围绕model层而不围绕DOM,即操作model就是操作dom,同时能让我们开发人员离开DOM都能轻松进行前端开发。avalon中定义的VM处理业务逻辑与提供数据源,HTML中的绑定负责渲染与响应用户点击拖拽等行为,这样就最大保证了视图逻辑相分离。