avalonJS是司徒正美開發和維護的前端mvvm框架,可以輕松實現數據的隔離和雙向綁定,相比angularJS等前端框架它有如下優勢:
1.壓縮后僅有60多kb,而angular的min版是100多kb;
2.兼容IE6+,符合天朝市場需求;
3.效率更高,跑起來比angular和knockout都要更快,在移動端上該優勢會更大(avalon有移動端專版的avalon.modern.js)。關於其性能更詳細的介紹可以看這里;
4.涵蓋了angular的大部分功能,且實現方式更為便捷、上手更容易;
5.有配套的UI庫(當然這個按需選擇即可),由司徒正美及其“去哪兒”團隊維護,有相關的中文文檔(下方會提到),除了在github提交issue,你也可以加入正美的Q群79641290 來交流問題或提交bug。
(這位兄台,谷歌送溫暖,開門查水表)
然而avalon也有自己的劣勢——知名度較低。不過畢竟國產的東西沒經BAT推廣,要像seaJS那樣馳名中外倒是不容易。
相關中文文檔
正美其實私下寫了不少avalon的官方api和教程,大家可以訪問如下地址:
GitHub(下載最新的avalon以及實例(examples文件夾里),通過實例來掌握某些功能的實現是很好的學習途徑)
Avalon快速入門(比較快捷的入門課程,只用了幾篇文章來介紹了最常用的一些功能)
API文章(正美的博文,篇幅較大,涵蓋知識點很多,可以當作API來查閱,只是正美的博客排版真的。。。看起來略吃力),也可以在這里查看更規范的API。
Avalon亂燉(強烈推薦,用了20多篇文章較詳細地、漸進地介紹avalon,必讀的就是啦)
Avalon入門視頻(推薦)
本系列初衷
雖然正美已經細心編寫了不少中文文檔,不過有的文章技術門檻有點高,不太適合初學者,另外作為avalon的用戶,以用戶的角度來較詳細地介紹avalon或許會更合適些。
本系列相比正美的教程,會更側重於“怎么用”,而非其機制或原理的介紹。
另外也希望本系列能為推廣avalon出一份綿薄之力,希望能讓更多的前端愛好者開始接觸avalon,並喜歡上這個前端利器。
本系列技術需求
本系列除了avalonJS之外,還會搭配requireJS做輔助,特別是后面我們會使用avalon的路由系統,來做一個單頁面站點(放到移動端就是SPA了),需要requireJS及其插件來按需加載腳本和樣式文件,故建議查閱本系列的朋友要多多少少會一點requireJS的知識。
個人還是覺得avalon搭配requireJS的話,在前端可謂hold住全場了(咱忽略node及其框架...)~
開始
我們可以在這里獲取最新版本的avalonJS,然后將其引入頁面中(本章先不考慮搭配requireJS,僅僅先玩一玩、介紹下):
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>初玩阿瓦隆</title> <script type="text/javascript" src="avalon.js"></script> </head> <body> <div></div> </body> </html>
接着,類似於ng的“ng-controller”,avalon的控制域屬性名叫做“ms-controller”,你可以把它當作一個監聽器,把它綁定到一個容器后,avalon就能掃描和監聽這個容器內的所有(綁定了avalon方法或帶有插值表達式的)元素了。
我們來給div加上這個監聽器,並在里面寫一個avalon插值表達式{{XXX}}:
<div ms-controller="wrap">{{a}}</div>
你現在運行它的話,還沒有任何效果,因為我們還沒有寫腳本來讓avalon工作起來。我們可以來這么一段簡單的腳本:
<body> <div ms-controller="wrap">{{a}}</div> <script type="text/javascript"> var abc = avalon.define({ //abc是隨便起的一個名字,用作該Model的載體 $id: "wrap", //告訴avalon這個Model是作用於哪個ms-controller的 a: "你好啊" //定義一個avalon對象屬性“a”,其值是“你好啊” }); avalon.scan(); //這句話可以不加,因為avalon有自己的DOMReady模塊,會自動掃描全文。 </script> </body>
在avalon中,我們用 avalon.define({ ... }) 的形式來定義一個Model實例(其參數可以看做一個avalon數據對象),其中的 $id 是內置屬性,對應所要掃描和監控的控制域名。
我們還在內部定義了一個屬性"a",故在對應的控制域(如這段代碼對應的域是綁定ms-controller="wrap"的div標簽)里,我們使用avalon插值表達式{{a}}的話,可以自動綁定其值“你好啊”。
注:最后一句 avalon.scan(); 可以不加上,不過后續我們會使用requireJS來配合使用avalon,屆時會建議刪掉avalon的DOMReady模塊,故先養成加上scan的習慣。
上述代碼運行效果如下:
當然,avalon有着更類似ng的寫法:
avalon.define("wrap", function(vm) { ...... })
但個人覺得沒必要,還是下方的寫法來的簡單(本系列后續的實例也將遵從該寫法):
var vm = avalon.define({ $id:wrap, ...... })
下面這段代碼可以幫你更好了解avalon的控制域:
<body> <div ms-controller="wrap">{{a}}</div> <div ms-controller="wrap2"> {{a}} <span>{{b}}</span> </div> <script type="text/javascript"> var abc = avalon.define({ $id: "wrap", a: "你好啊" }); var def = avalon.define({ $id: "wrap2", a: "大家好", b: "哈哈哈" }); avalon.scan(); </script> </body>
執行效果如下:
兩個作用域(ms-controller)之間可以互相訪問彼此的數據,還記得我們給avalon.define的前面定義了一個載體么(var XX = avalon.define),利用它就能輕松獲取:
<body> <div ms-controller="wrap">{{a}}</div> <div ms-controller="wrap2"> {{a}} <span>{{b}}</span> </div> <script type="text/javascript"> var abc = avalon.define({ $id: "wrap", a: "你好啊" }); var def = avalon.define({ $id: "wrap2", a: "大家好", b: abc.a //獲取第一個Model里的屬性值 }); avalon.scan(); </script> </body>
執行效果:
數據和視圖同步
上方我們實現了非常簡單的數據綁定,將一個avalon對象屬性a綁定到DOM元素上。不過avalon更有趣和實用的地方是它實現了數據與視圖的同步,說的簡單點,我們用腳本修改了a的值,那么DOM上綁定的數據也會跟着改變(當然反過來也是一樣的):
<body> <div ms-controller="wrap"> <span>{{a}}</span> <input ms-duplex="a" /> </div> <script type="text/javascript"> var abc = avalon.define({ $id: "wrap", a: "你好啊" }); avalon.scan(); </script> </body>
注意我們這里增加了一個 <input ms-duplex="a" /> ,其中的 ms-duplex 是avalon的雙工綁定屬性,它除了負責將VM中對應的值(如本例是a)放到表單元素的value中,還對元素偷偷綁定一些事件,用於監聽用戶的輸入從而自動刷新VM。
執行如下:
實例
利用avalon數據-視圖同步的特性,我們可以更便捷地、更少代碼地實現某些功能。舉個例子,我們來實現一個選項卡的功能:
如上圖的選項卡你會如何實現呢?可能你會寫兩個ul來對應下方兩個選項卡列表,每個ul里都寫上4個li(或者讓后端人員通過后端框架來寫loop,從而動態生成li),然后你再把第二個ul隱藏了,接着寫個方法,讓鼠標移到第二個選項卡標題時,第一個ul隱藏,第二個ul顯示,對吧。
還有右上角的 “更多XX” 的連接,也可以通過隱藏-顯示的方式來實現
你的DOM代碼可能是這樣的:
<div> <span id="gg">公告</span><span id="bd">媒體報道</span> <a id="more_gg" href="#!/gg">更多公告</a><a id="more_bd" href="#!/bd">更多報道</a> <ul id="gg_list"> <li><a href="#!/gg/1" title="公告文章標題1">公告文章標題1</a></li> <li><a href="#!/gg/2" title="公告文章標題2">公告文章標題2</a></li> <li><a href="#!/gg/3" title="公告文章標題3">公告文章標題3</a></li> <li><a href="#!/gg/4" title="公告文章標題4">公告文章標題4</a></li> </ul> <ul id="bd_list"> <li><a href="#!/bd/1" title="媒體報道文章標題1">媒體報道文章標題1</a></li> <li><a href="#!/bd/2" title="媒體報道文章標題2">媒體報道文章標題2</a></li> <li><a href="#!/bd/3" title="媒體報道文章標題3">媒體報道文章標題3</a></li> <li><a href="#!/bd/4" title="媒體報道文章標題4">媒體報道文章標題4</a></li> </ul> </div>
但使用avalon的話,一切都更簡單:
<div ms-controller="list"> <span ms-mouseover="changeUl(gg)">公告</span> <span ms-mouseover="changeUL(bd)">媒體報道</span> <a ms-href="'#!/'+ more_name">{{more_text}}</a> <ul> <li ms-repeat="infoList"> <a ms-href="'#!/'+ more_name + '/' + el.id" ms-title="el.title">{{el.title}}</a> </li> </ul> </div>
首先它只有一個“更多XX”的<a>標簽,而且只有一個<ul>,而且不存在任何后端標簽的介入就能實現數據循環綁定。
它的優勢在數據越多的時候會越明顯(例如每個ul要顯示100條li)。
我們來看看avalon的腳本應當怎么寫:
<body> <script type="text/javascript"> var gg=[{"id":"1","title":"公告文章標題1"},{"id":"2","title":"公告文章標題2"},{"id":"3","title":"公告文章標題3"},{"id":"4","title":"公告文章標題4"}]; var bd=[{"id":"1","title":"媒體報道文章標題1"},{"id":"2","title":"媒體報道文章標題2"},{"id":"3","title":"媒體報道文章標題3"},{"id":"4","title":"媒體報道文章標題4"}]; </script> <div ms-controller="list"> <span ms-mouseover="changeUl(1)">公告</span> <span ms-mouseover="changeUl(0)">媒體報道</span> <a ms-href="'#!/'+ more_name">{{more_text}}</a> <ul> <li ms-repeat="infoList"> <a ms-href="'#!/'+ more_name + '/' + el.id" ms-title="el.title">{{el.title}}</a> </li> </ul> </div> <script type="text/javascript"> var vm = avalon.define({ $id: "list", more_name: "gg", more_text: "更多公告", gg:gg, bd:bd, infoList:gg, changeUl:function(flag){ if(flag){ //鼠標移過“公告”選項卡頭部 vm.more_name = "gg"; vm.more_text = "更多公告"; vm.infoList = vm.gg; }else{ //鼠標移過“媒體報道”選項卡頭部 vm.more_name = "bd"; vm.more_text = "更多報道"; vm.infoList = vm.bd; } } }); avalon.scan(); </script> </body>
執行效果:
我們來逐步分析下上方的代碼。首先看第一段腳本:
<script type="text/javascript"> var gg=[{"id":"1","title":"公告文章標題1"},{"id":"2","title":"公告文章標題2"},{"id":"3","title":"公告文章標題3"},{"id":"4","title":"公告文章標題4"}]; var bd=[{"id":"1","title":"媒體報道文章標題1"},{"id":"2","title":"媒體報道文章標題2"},{"id":"3","title":"媒體報道文章標題3"},{"id":"4","title":"媒體報道文章標題4"}]; </script>
這里的 gg 表示“公告”的列表JSON數據,bd 則是“媒體報道”的列表JSON數據,你可以讓后端的朋友直接在此處提供JSON數據過來。我們后續會利用avalon把這些數據綁定到頁面視圖上。
我們再看DOM結構:
<div ms-controller="list"> <span ms-mouseover="changeUl(1)">公告</span> <span ms-mouseover="changeUl(0)">媒體報道</span> <a ms-href="'#!/'+ more_name">{{more_text}}</a> <ul> <li ms-repeat="infoList"> <a ms-href="'#!/'+ more_name + '/' + el.id" ms-title="el.title">{{el.title}}</a> </li> </ul> </div>
<span>中的 ms-mouseover 是avalon的“onmouseover”方法,其值 changeUl(X) 是我們在最后的avalon腳本中定義的一個事件方法,然后比如當鼠標移到“媒體報道”的span上,會觸發綁定是 changeUl(0) 事件。
我們再看看 <a ms-href="'#!/'+ more_name">{{more_text}}</a> 這里的 ms-href 自然也是avalon中的“href”屬性,可以植入avalon對象屬性(如這里的more_name),也可以加上字符串(如這里的'#!/'),但要用引號括起來,不然會被當作avalon對象屬性處理。
接着是最重要的部分:
<li ms-repeat="infoList"> <a ms-href="'#!/'+ more_name + '/' + el.id" ms-title="el.title">{{el.title}}</a> </li>
我們使用了 ms-repeat="XX" 屬性來綁定要重復顯示的哈希數據,同時會生成一個代理VM對象,該代理對象擁有el,$index, $first, $last, $remove 等屬性(點這里查看詳細),其中我們用到的 el 表示指向當前的數據元素,從而可以通過 el.id ,el.title 來獲取infoList數組對象的具體元素。
最后咱再看看avalon腳本:
<script type="text/javascript"> var vm = avalon.define({ $id: "list", more_name: "gg", more_text: "更多公告", gg:gg, //獲取公告JSON數據 bd:bd, //獲取媒體報道JSON數據 infoList:gg, //infoList缺省值為公告JSON數據 changeUl:function(flag){ if(flag){ //鼠標移過“公告”選項卡頭部 vm.more_name = "gg"; vm.more_text = "更多公告"; vm.infoList = vm.gg; //infoList變為公告JSON數據 }else{ //鼠標移過“媒體報道”選項卡頭部 vm.more_name = "bd"; vm.more_text = "更多報道"; vm.infoList = vm.bd; //infoList變為媒體報道JSON數據 } } }); avalon.scan(); </script>
這里要注意的是我們用了
gg:gg, //獲取公告JSON數據 bd:bd, //獲取媒體報道JSON數據
來獲取和存儲“公告/媒體報道”的JSON數據到avalon對象的屬性中(左側的gg和bd是avalon對象屬性,右側的gg和bd是全局變量),這樣做的原因是后續的回調事件changeUl(flag)要通過參數來判斷和修改vm.infoList的值,而其值應同為avalon對象屬性。如果把代碼改為這樣會出錯(剛用avalon的朋友可能就會這樣寫):
<script type="text/javascript"> var vm = avalon.define({ $id: "list", more_name: "gg", more_text: "更多公告", infoList:gg, //infoList缺省值為公告JSON數據 changeUl:function(flag){ if(flag){ //鼠標移過“公告”選項卡頭部 vm.more_name = "gg"; vm.more_text = "更多公告"; vm.infoList = gg; }else{ //鼠標移過“媒體報道”選項卡頭部 vm.more_name = "bd"; vm.more_text = "更多報道"; vm.infoList = bd; } } }); avalon.scan(); </script>
執行效果如下:
是的,鼠標第一次移上去的時候是無誤的,但再移到其它選項卡的時候就不按常理出牌了,這是為什么?
這是因為當我們定義 “vm.infoList=gg” 時,vm.infoList 自然指向了 gg(注意gg是一個對象類型),若vm.infoList的值改變了,那么其指向的 gg 對象的值自然也會跟着改變。你可以這樣來調試:
var vm = avalon.define({ $id: "list", more_name: "gg", more_text: "更多公告", infoList:gg, changeUl:function(flag){ console.table(gg); //console出“公告”變量的數據信息 if(flag){ vm.more_name = "gg"; vm.more_text = "更多公告"; vm.infoList = gg; }else{ vm.more_name = "bd"; vm.more_text = "更多報道"; vm.infoList = bd; } } }); avalon.scan();
結果(第一次回調事件的gg是正常的,但第二次開始就變成了媒體報道的數據):
因此我們要記得,若存在外部引入的數據,應用一個avalon對象屬性保存起來。
第一篇入門文章就到這里,下一篇我們開始以配合requireJS的形式來使用avalon。
共勉~