背景

在运营活动开发中,因为工作的重复性很大,同时往往开发时间短,某些情况下也会非常紧急,导致了活动开发时间被大大压缩,同时有些活动逻辑复杂,数据或者状态变更都需要手动渲染,容易出错,正是因为这些问题的存在,所以才有了MV*框架的诞生,比如大名鼎鼎的angularJS。今天就跟大家讲讲国产的MVVM框架avalonJS是如何快速进行开发的,同时大家也可以对比石器时代的开发模式(jquery或者zepto)与mv*模式的区别。

avalonJS简介

avalonJS是前端大牛司徒正美开发和维护的mvvm框架,它是一个基于Model驱动的开发框架,DOM操作近乎绝迹,可以让前端人员脱离DOM的苦海,来到数据的乐园,相比angularJS它有如下优势:
1.无任何依赖,压缩后只有50多kb,而angular的min版有100多kb;

2.爽快的编程体验,不再纠结于DOM操作;

3.兼容到IE6+,符合天朝国情;

4.效率更高,跑起来比angular和knockout都要更快,在移动端上该优势会更大(avalon有移动端专版的avalon.modern.js)。关于其性能更详细的介绍可以看这里

5.涵盖了angular的大部分功能,且实现方式更为便捷、上手更容易;

相关文档

GitHub(下载最新的avalon以及实例(examples文件夹里),通过实例来掌握某些功能的实现是很好的学习途径)

Avalon快速入门(比较快捷的入门课程,只用了几篇文章来介绍了最常用的一些功能)

API文章(正美的博文,篇幅较大,涵盖知识点很多,可以当作API来查阅),也可以在这里查看更规范的API。

Avalon乱炖(强烈推荐,用了20多篇文章较详细地、渐进地介绍avalon)

Avalon入门视频(推荐)

开始 

这里使用avalon的版本是移动端avalon.modern.shim.js-1.4.1版本,已经存在cdn上(http://imgcache.gtimg.cn/club/common/lib/avalon.js),需要使用直接引入即可,如下:

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>avalon初探</title>
</head>
<body>
<div></div> <script src="http://imgcache.gtimg.cn/c/=/club/mobile_web/zepto.min.js,/club/common/lib/zero/zero.m.min-5.1.1.js,/club/common/lib/avalon.js?max_age=86400000"></script>
</body>
</html>

这里我们引入了zepto,zero以及avalon.js,因为zero依赖zepto,所以这几个文件必须要引入。

接着,类似于ng的“ng-controller”,avalon的控制域属性名叫做“ms-controller”,你可以把它当作一个监听器,把它绑定到一个容器后,avalon就能扫描和监听这个容器内所有(绑定了avalon方法或带有插值表达式的)元素了。

我们给这个div加上这个监听器,并在里面写一个avalon插值表达式{{a}};

<div ms-controller="wrap">{{a}}</div>

你现在运行的话页面没有任何效果,因为我们还没有写脚本让avalon工作起来,我们可以来一段简单代码让其运行起来:

<div ms-controller="wrap">{{a}}</div>

<script src="http://imgcache.gtimg.cn/c/=/club/mobile_web/zepto.min.js,/club/common/lib/zero/zero.m.min-5.1.1.js,/club/common/lib/avalon.js?max_age=86400000"></script>
<script>
var model = avalon.define('wrap', function (vm) { //model是随便命名的,用作该Model的载体,wrap为avalon的作用域名称
vm.a = '你好啊'; //a为avalon定义的一个属性,其值为“你好啊”
});
avalon.scan(); //开启avalon扫描,这句话必须要加
</script>
</body>

在avalon中我们使用avalon.define('xx', function (vm) {})来定义一个Model实例,其中xx为所要扫描和监控的控制域名。

我们还在内部定义了一个属性“a”,故在对应的控制域(对应为ms-controller=“wrap”的div)里 ,我们使用avalon插值表达式{{a}}的话,可以自动绑定其值“你好啊”。

上述代码运营效果如下:

使用MVVM框架(avalonJS)进行快速开发-LMLPHP

数据和视图同步

上方我们实现了非常简单的数据绑定,将一个avalon属性a绑定到DOM元素上。不过,avalon更有意思和实用的功能是实现了视图和数据的同步,说的简单点,我们用脚本修改了a的值,那么DOM上绑定的数据也会跟着改变(反过来也一样)

<div ms-controller="wrap">
<span>{{a}}</span>
<input ms-duplex="a" />
</div> <script src="http://imgcache.gtimg.cn/c/=/club/mobile_web/zepto.min.js,/club/common/lib/zero/zero.m.min-5.1.1.js,/club/common/lib/avalon.js?max_age=86400000"></script>
<script>
var model = avalon.define('wrap', function (vm) {
vm.a = '你好啊';
});
avalon.scan();
</script>
</body>

注意这里我们添加了一个<input ms-duplex="a" />,其中ms-duplex是avalon的双工绑定的属性,它除了负责将VM中对应的值(如本例是a)放到表单元素的value中,还偷偷对元素绑定一些事件,用于监听用户的输入从而自动刷新VM。

执行代码如下:

使用MVVM框架(avalonJS)进行快速开发-LMLPHP

运营页面的开发

有了以上对avalon的基本了解,我们来看下在运营活动中如何使用avalon快速开发运营活动。

这里以QGC情报站为例,这个页面完全是一个静态页面,所有的数据都是在ams上手动配置的,先看下页面截图:

使用MVVM框架(avalonJS)进行快速开发-LMLPHP

一个如此简单的页面,如果我们使用zepto来处理,可能会涉及到各种字符串拼接,然后通过innerHTML的方式插入到指定的div内,这些过程势必要选择多个dom,然后操作dom,如果还需要处理一些其他细节需求,比如根据ams有没有配置更多链接,来动态决定是否显示“更多数据”按钮,等等等等。我们发现使用zepto的方式来写代码很繁琐,更新页面状态时总是需要首先获取到dom,然后对dom进行其他操作,如果一个dom有多种状态,又在不同地方展示,那我们的代码量就会递增式增长,同时代码可读性差,不易维护。

而使用avalon来处理,我们处理的仅仅是数据,代码量不仅会大幅减少,而且代码结构会更清晰,也更易维护。

直接上代码:

html代码:

<div class="act-wrapper" ms-controller="main">
<div class="act-content">
<div class="act-header">
<h1 class="hide">全民竞技大赛</h1>
<div class="nav">
<a href="javascript:" ms-class="cur:index==$index" ms-repeat="curGames" ms-click="switchTab($index, el.appid)">{{el.gameName}}</a>
</div>
</div> <div class="act-game">
<div class="act-main">
<div class="act-block act-news">
<div class="title">
<b class="t1"><strong class="hide">火线礼包</strong></b>
<a href="javascript:" ms-click="openUrl(curOnlineLink.link)">赛事直播>></a>
</div>
<div class="cnts">
<div class="item" ms-repeat="curNews">
<a href="javascript:" ms-click="openUrl(el.link)" ms-if="$index==0"><img ms-attr-src="el.pic" width="281"/></a>
<a href="javascript:" ms-click="openUrl(el.link)" ms-if="$index">
<div class="thumb"><img ms-attr-src="el.pic" width="80"/></div>
<p>{{el.title}}</p>
</a>
</div>
</div>
</div> <div class="act-block act-ranking">
<div class="title">
<b class="t2"><strong class="hide">赛事数据</strong></b>
<a href="javascript:" ms-click="openUrl(curDataMoreLink.link)" ms-if="curDataMoreLink.link!=''">更多数据>></a>
</div>
<div class="cnts">
<div class="item" ms-repeat="curData">
<span>{{el.name}}</span>
<p>{{el.achievement}}</p>
</div>
</div>
</div> <div class="act-block act-events">
<div class="title">
<b class="t3"><strong class="hide">赛事活动</strong></b>
</div>
<div class="cnts">
<div class="act-slider">
<div class="swiper-container gallery">
<div class="swiper-wrapper">
<div class="swiper-slide" ms-repeat="curAct">
<a href="javascript:" ms-click="openUrl(el.link)">
<img ms-attr-src="el.pic" width="255"/>
</a>
</div>
</div>
</div>
<div class="swiper-pagination"></div>
</div>
</div>
</div> <div class="act-block act-god">
<div class="title">
<b class="t4"><strong class="hide">大神助力</strong></b>
<a href="javascript:" ms-click="openUrl(curGodLink.link)">大神名录>></a>
</div>
<div class="cnts">
<div class="list">
<dl>
<dd ms-repeat="curQuot" ms-click="openUrl(el.link)">
<div class="avatar"><img ms-attr-src="el.pic" width="51"/></div>
<div class="info">
<b>{{el.name}}</b>
<p>{{el.content}}</p>
</div>
</dd>
</dl>
</div>
</div>
</div> <div class="act-video">
<div class="nav">
<a href="javascript:" ms-class="cur:videoIndex==0" ms-click="switchVideoTab(0, 1)">赛事视频</a>
<a href="javascript:" ms-class="cur:videoIndex==1" ms-click="switchVideoTab(1, 2)">精彩集锦</a>
<a href="javascript:" ms-class="cur:videoIndex==2" ms-click="switchVideoTab(2, 3)">大神视角</a>
</div>
<div class="cnts">
<div class="cnt">
<a href="javascript:" ms-repeat="curVideo" ms-click="openUrl(el.link)">
<img ms-attr-src="el.pic" width="123"/>
<p>{{el.content}}</p>
</a>
</div>
<a href="javascript:" class="act-more" ms-click="openUrl(videoMore)">更多视频>></a>
</div>
</div>
</div>
<div class="act-footer">
<div class="act-btn-group">
<a href="javascript:" ms-click="openUrl(btLink.more_game)">更多赛事</a>
<a href="javascript:" ms-click="openUrl(btLink.gamecenter)">游戏中心</a>
</div>
<p class="game-info">手机QQ游戏中心出品</p>
</div>
</div>
</div> <div class="act-bg">
<img src="http://imgcache.gtimg.cn/vipstyle/game/act/breezefeng/20151204_qgc/img/bg_01.jpg" width="320"/>
<img src="http://imgcache.gtimg.cn/vipstyle/game/act/breezefeng/20151204_qgc/img/bg_02.jpg" width="320"/>
<img src="http://imgcache.gtimg.cn/vipstyle/game/act/breezefeng/20151204_qgc/img/bg_03.jpg" width="320"/>
</div>
</div> <script src="http://imgcache.gtimg.cn/c/=/club/mobile_web/zepto.min.js,/club/common/lib/zero/zero.m.min-5.1.1.js,/club/common/lib/avalon.js?max_age=86400000"></script>

js代码:

new qv.zero.Page({
jsonid: '76172',
mqqEnv: true,
game: 'cfm',
onlyMobile: true,
isOpenSQView: true,
redirectUrl: "",
preloads: ['mqqShare'],
afterInit: function () {
var me = this; qv.zero.Login.ensure(); qv.zero.mqqShare.initShare(); me.defineModel();
me.getData();
me.initData();
},
defineModel: function () {
var me = this;
main = avalon.define('main', function (vm) {
//当前游戏appid
vm.appid = 1104067326;
//当前游戏tab索引
vm.index = 0;
//切换游戏tab索引
vm.switchTab = function (index, appid) {
main.index = index;
main.appid = appid;
me.changeData(appid);
}; //当前视频tab索引
vm.videoIndex = 0;
//当前视频类型
vm.videoType = 1;
//切换视频tab索引
vm.switchVideoTab = function (videoIndex, videoType) {
main.videoIndex = videoIndex;
main.videoType = videoType;
console.log(me.getVideoByType(main.$videos[main.appid] || []));
main.curVideo = me.getVideoByType(main.$videos[main.appid] || []);
main.videoMore = main.curVideo[0] && main.curVideo[0].more_link || '';
}; //打开链接
vm.openUrl = function (url) {
me.openUrl(url);
}; //当前游戏
vm.curGames = []; //赛事直播连接
vm.$onlineLinks = {};
vm.curOnlineLink = {}; //赛事新闻
vm.$news = {};
vm.curNews = []; //赛事数据的更多数据
vm.$dataMoreLinks = {};
vm.curDataMoreLink = {}; //赛事数据
vm.$data = {};
vm.curData = []; //赛事活动
vm.$act = {};
vm.curAct = []; //大神名录链接
vm.$godLinks = {};
vm.curGodLink = {}; //大神语录
vm.$quot = {};
vm.curQuot = []; //赛事视频
vm.$videos = {};
vm.curVideo = []; //赛事视频更多链接
vm.videoMore = ''; //底部链接
vm.btLink = {};
});
},
changeData: function (appid) {
var me = this;
main.curOnlineLink = main.$onlineLinks[appid] && main.$onlineLinks[appid][0] || {};
main.curNews = main.$news[appid] || [];
main.curDataMoreLink = main.$dataMoreLinks[appid] && main.$dataMoreLinks[appid][0] || {};
main.curData = main.$data[appid] || [];
main.curAct = main.$act[appid] || [];
main.curGodLink = main.$godLinks[appid] && main.$godLinks[appid][0] || {};
main.curQuot = main.$quot[appid] || [];
main.curVideo = me.getVideoByType(main.$videos[appid] || []);
main.videoMore = main.curVideo[0] && main.curVideo[0].more_link || ''; setTimeout(function () {
mySwiper.update();
}, 30);
},
getData: function () {
var me = this;
//赛事直播链接
main.$onlineLinks = me.getAmsData(2);
//赛事新闻
main.$news = me.getAmsData(3);
//赛事数据的更多数据
main.$dataMoreLinks = me.getAmsData(4);
//赛事数据
main.$data = me.getAmsData(5);
//赛事活动
main.$act = me.getAmsData(6);
//大神名录链接
main.$godLinks = me.getAmsData(7);
//大神语录
main.$quot = me.getAmsData(8);
//赛事视频
main.$videos = me.getAmsData(9);
},
initData: function () {
var me = this;
main.curOnlineLink = main.$onlineLinks[main.appid] && main.$onlineLinks[main.appid][0] || {};
main.curNews = main.$news[main.appid] || [];
main.curDataMoreLink = main.$dataMoreLinks[main.appid] && main.$dataMoreLinks[main.appid][0] || {};
main.curData = main.$data[main.appid] || [];
main.curAct = main.$act[main.appid] || [];
main.curGodLink = main.$godLinks[main.appid] && main.$godLinks[main.appid][0] || {};
main.curQuot = main.$quot[main.appid] || [];
main.btLink = zMsg.getFormData(10)[0];
main.curVideo = me.getVideoByType(main.$videos[main.appid] || []);
main.videoMore = main.curVideo[0] && main.curVideo[0].more_link || '';
main.curGames = zMsg.getFormData(2); setTimeout(function () {
mySwiper.update();
}, 30);
$('#loading').css('display', 'none');
avalon.scan();
},
getVideoByType: function (videos) {
var arr = [];
for (var i = 0, len = videos.length; i< len; i++) {
(videos[i].type == main.videoType) && (arr.push(videos[i]));
}
return arr;
},
getAmsData: function (id) {
return this.convert2ObjByAppId(zMsg.getFormData(id));
},
convert2ObjByAppId: function (arr) {
var i, cur, len, obj = {};
for (i = 0, len = arr.length; i < len; i++) {
cur = arr[i];
(obj[cur.appid] || (obj[cur.appid] = [])).push(cur);
}
return obj;
},
setShareData: function () {
var shareObj = zMsg.getFormData(11)[0];
return {
title: shareObj.title,//分享标题
desc: shareObj.content, //分享内容
imageUrl: shareObj.pic, //分享图片
shareUrl: location.href,
back: true//发送消息之后是否返回到web页面
}
},
//手Q打开链接
openUrl: function (url, target) {
var targetUrl = url;
target = target || 1;
if (typeof(mqq) != "undefined" && mqq.QQVersion != 0) {
mqq.ui.openUrl({url: targetUrl, target: target, style: 0});
} else {
window.location.href = targetUrl;
}
}
});

乍一看代码好多,感觉很复杂的样子,实际上这里因为模块比较多,所以代码量稍微有点大,大家看得时候按模块来看就很容易理解了。

我们看到使用avalon后代码结构非常清晰,html结构也很清晰,基本上看不到DOM操作。这里我们会看到很多以ms-开头的指令,比如ms-click是绑定click事件,ms-repeat是循环输出数组列表等,相关指令可以去上面推荐的文档内查阅,这里只是给大家如何使用avalon做一些抛砖引玉,avalon使用门槛很低,大家也可以多尝试下,有问题欢迎随时交流!

最后附上QGC情报站的页面交互效果图:

使用MVVM框架(avalonJS)进行快速开发-LMLPHP

04-15 04:59