背景
在運營活動開發中,因為工作的重復性很大,同時往往開發時間短,某些情況下也會非常緊急,導致了活動開發時間被大大壓縮,同時有些活動邏輯復雜,數據或者狀態變更都需要手動渲染,容易出錯,正是因為這些問題的存在,所以才有了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}}的話,可以自動綁定其值“你好啊”。
上述代碼運營效果如下:
數據和視圖同步
上方我們實現了非常簡單的數據綁定,將一個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。
執行代碼如下:
運營頁面的開發
有了以上對avalon的基本了解,我們來看下在運營活動中如何使用avalon快速開發運營活動。
這里以QGC情報站為例,這個頁面完全是一個靜態頁面,所有的數據都是在ams上手動配置的,先看下頁面截圖:
一個如此簡單的頁面,如果我們使用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情報站的頁面交互效果圖: