avalon是一個簡單易用迷你的MVVM框架,它最先發佈於2012.09.15,爲解決同一業務邏輯存在各類視圖呈現而開發出來的。 事實上,這問題其實也能夠簡單地利用通常的前端模板加jQuery 事件委託 搞定,但隨着業務的膨脹, 代碼就充滿了各類選擇器與事件回調,難以維護。所以完全的將業務與邏輯分離,就只能求助於架構。 最初想到的是MVC,嘗試過backbone,但代碼不降反升,很偶爾的機會,碰上微軟的WPF, 優雅的MVVM架構當即吸引住我,我以爲這就是我一直追求的解決之道。javascript
MVVM將全部前端代碼完全分紅兩部分,視圖的處理經過綁定實現(angular有個更炫酷的名詞叫指令), 業務邏輯則集中在一個個叫VM的對象中處理。咱們只要操做VM的數據,它就天然而然地神奇地同步到視圖。 顯然全部神祕都有其內幕,C#是經過一種叫訪問器屬性的語句實現,那麼JS也有沒有對應的東西。 感謝上帝,IE8最先引入這東西(Object.defineProperty),惋惜有BUG,但帶動了其餘瀏覽器實現它,IE9+便能安全使用它。 對於老式IE,我找了很久,實在沒有辦法,使用VBScript實現了。css
Object.defineProperty或VBS的做用是將對象的某一個屬性,轉換一個setter與getter, 咱們只要劫持這兩個方法,經過Pub/Sub模式就能偷偷操做視圖。爲了記念WPF的指引,我將此項目以WPF最初的開發代號avalon來命名。 它真的能讓前端人員脫離DOM的苦海,來到數據的樂園中!html
絕對的優點就是下降了耦合, 讓開發者從複雜的各類事件中掙脫出來。 舉一個簡單地例子, 同一個狀態可能跟若干個事件的發生順序與發生時的附加參數都有關係, 不用 MVC (包括 MVVM) 的狀況下, 邏輯可能很是複雜並且脆弱。 而且一般須要在不一樣的地方維護相關度很是高的一些邏輯, 稍有疏忽就會釀成 bug 不能自拔。使用這類框架能從根本上下降應用開發的邏輯難度, 而且讓應用更穩健。前端
除此以外, 也免去了一些重複的體力勞動, 一個 {value} 就代替了一行 $(selector).text(value)。 一些個經常使用的 directive 也能快速實現一些本來可能須要較多代碼才能實現的功能java
avalon如今有三個分支:avalon.js 兼容IE6,標準瀏覽器, 及主流山寨瀏覽器(QQ, 獵豹, 搜狗, 360, 傲遊); avalon.modern.js 則只支持IE10等支持HTML5現代瀏覽器 ; avalon.mobile.js,添加了觸屏事件與fastclick支持,用於移動端node
咱們從一個完整的例子開始認識 avalon :jquery
<!DOCTYPE html> <html> <head> <title></title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script src="avalon.js"></script> </head> <body> <div ms-controller="box"> <div style=" background: #a9ea00;" ms-css-width="w" ms-css-height="h" ms-click="click"></div> <p>{{ w }} x {{ h }}</p> <p>W: <input type="text" ms-duplex="w" data-duplex-event="change"/></p> <p>H: <input type="text" ms-duplex="h" /></p> </div> <script> var vm = avalon.define({ $id: "box", w: 100, h: 100, click: function() { vm.w = parseFloat(vm.w) + 10; vm.h = parseFloat(vm.h) + 10; } }) </script> </body> </html>
上面的代碼中,咱們能夠看到在JS中,沒有任何一行操做DOM的代碼,也沒有選擇器,很是乾淨。在HTML中, 咱們發現就是多了一些以ms-開始的屬性與{{}}標記,有的是用於渲染樣式, 有的是用於綁定事件。這些屬性或標記,實質就是avalon的綁定系統的一部分。綁定(有的框架也將之稱爲指令), 負責幫咱們完成視圖的各類操做,至關於一個隱形的jQuery。正由於有了綁定,咱們就能夠在JS代碼專一業務邏輯自己, 寫得更易維護的代碼! git
不過上面的代碼並不完整,它能工做,是由於框架默認會在DOMReady時掃描DOM樹,將視圖中的綁定屬性與{{}}插值表達式抽取出來,轉換爲求值函數與視圖刷新函數。github
上面的JS代碼至關於:ajax
avalon.ready(function() { var vm = avalon.define({ $id: "box", w: 100, h: 100, click: function() { vm.w = parseFloat(vm.w) + 10; vm.h = parseFloat(vm.h) + 10; } }) avalon.scan() })
avalon.scan是一個很是重要的方法,它有兩個可選參數,第一個是掃描的起點元素,默認是HTML標籤,第2個是VM對象。
//源碼 avalon.scan = function(elem, vmodel) { elem = elem || root var vmodels = vmodel ? [].concat(vmodel) : [] scanTag(elem, vmodels) }
視圖模型,ViewModel,也常常被略寫成VM,是經過avalon.define方法進行定義。生成的對象會默認放到avalon.vmodels對象上。 每一個VM在定義時必須指定$id。若是你有某些屬性不想監聽,能夠直接將此屬性名放到$skipArray數組中。
var vm = avalon.define({ $id: "test", a: 111, b: 222, $skipAarray: ["b"], $c: 333, firstName: "司徒", lastName: "正美", fullName: {//一個包含set或get的對象會被當成PropertyDescriptor, set: function(val) {//裏面必須用this指向scope,不能使用scope var array = (val || "").split(" "); this.firstName = array[0] || ""; this.lastName = array[1] || ""; }, get: function() { return this.firstName + " " + this.lastName; } }, array: [1,2,3], array2:[{e: 1}, {e: 2}] d: { k: 111, $skipArray: ["f"], f: 2222 } })
接着咱們說一些重要的概念:
$skipArray 是一個字符串數組,只能放當前對象的直接屬性名,想禁止子對象的某個屬性的監聽,在那個子對象上再添加一個$skipAray數組就好了。
視圖裏面,咱們可使用ms-controller, ms-important指定一個VM的做用域。
此外,在ms-each, ms-with,ms-repeat綁定屬性中,它們會建立一個臨時的VM,咱們稱之爲代理VM, 用於放置$key, $val, $index, $last, $first, $remove等變量或方法。
另外,avalon不容許在VM定義以後,再追加新屬性與方法,好比下面的方式是錯誤的:
var vm = avalon.define({ $id: "test", test1: "點擊測試按鈕沒反應 綁定失敗"; }); vm.one = function() { vm.test1 = "綁定成功"; };
也不容許在define裏面直接調用方法或ajax
avalon.define("test", function(vm){ alert(111) //這裏會執行兩次 $.ajax({ //這裏會發出兩次請來 async:false, type: "post", url: "sdfdsf/fdsfds/dsdd", success: function(data){ console.log(data) avalon.mix(vm, data) } }) })
應該改爲:
var vm = avalon.define({ $id: "test", aaa: "", //這裏應該把全部AJAX都返回的數據都定義好 bbb: "", }) $.ajax({ //這裏會發出兩次請來 async:false, type: "post", url: "sdfdsf/fdsfds/dsdd", success: function(data){ for(var i in data){ if(vm.hasOwnProperty(i)){ vm[i] = data[i] } } } })
咱們再看看如何更新VM中的屬性(重點):
<script> var model : avalon.define({ $id: "update", aaa : "str", bbb : false, ccc : 1223, time : new Date, simpleArray : [1, 2, 3, 4], objectArray : [{name: "a"}, {name: "b"}, {name: "c"}, {name: "d"}], object : { o1: "k1", o2: "k2", o3: "k3" }, simpleArray : [1, 2, 3, 4], objectArray : [{name: "a", value: "aa"}, {name: "b", value: "bb"}, {name: "c", value: "cc"}, {name: "d", value: "dd"}], object : { o1: "k1", o2: "k2", o3: "k3" } }) setTimeout(function() { //若是是更新簡單數據類型(string, boolean, number)或Date類型 model.aaa = "這是字符串" model.bbb = true model.ccc = 999999999999 var date = new Date model.time = new Date(date.setFullYear(2005)) }, 2000) setTimeout(function() { //若是是數組,注意保證它們的元素的類型是一致的 //只能全是字符串,或是全是布爾,不能有一些是這種類型,另外一些是其餘類型 //這時咱們可使用set方法來更新(它有兩個參數,第一個是index,第2個是新值) model.simpleArray.set(0, 1000) model.simpleArray.set(2, 3000) model.objectArray.set(0, {name: "xxxxxxxxxxxxxxxx", value: "xxx"}) }, 2500) setTimeout(function() { model.objectArray[1].name = "5555" }, 3000) setTimeout(function() { //若是要更新對象,直接賦給它一個對象,注意不能將一個VM賦給它,能夠到VM的$model賦給它(要不會在IE6-8中報錯) model.object = { aaaa: "aaaa", bbbb: "bbbb", cccc: "cccc", dddd: "dddd" } }, 3000) </script> <div ms-controller="update"> <div>{{aaa}}</div> <div>{{bbb}}</div> <div>{{ccc}}</div> <div>{{time | date("yyyy - MM - dd mm:ss")}}</div> <ul ms-each="simpleArray"> <li>{{el}}</li> </ul> <div> <select ms-each="objectArray"> <option ms-value="el.value">{{el.name}}</option> </select> </div> <ol ms-with="object"> <li>{{$key}} {{$val}}</li> </ol> </div>
avalon的綁定(或指令),擁有如下三種類型:
ms-skip //這個綁定屬性沒有值 ms-controller="expr" //這個綁定屬性沒有參數 ms-if="expr" //這個綁定屬性沒有參數 ms-if-loop="expr" //這個綁定屬性有一個參數 ms-repeat-el="array" //這個綁定屬性有一個參數 ms-attr-href="xxxx" //這個綁定屬性有一個參數 ms-attr-src="xxx/{{a}}/yyy/{{b}}" //這個綁定屬性的值包含插值表達式,注意只有少部分表示字符串類型的屬性可使用插值表達式 ms-click-1="fn" //這個綁定屬性的名字最後有數字,這是方便咱們綁定更多點擊事件 ms-click-2="fn" ms-click-3="fn" ms-on-click="fn" //只有表示事件與類名的綁定屬性的能夠加數字,如這個也能夠寫成 ms-on-click-0="fn" ms-class-1="xxx" ms-class-2="yyy" ms-class-3="xxx" //數字還表示綁定的次序 ms-css-background-color="xxx" //這個綁定屬性有兩個參數,但在css綁定裏,至關於一個,會內部轉換爲backgroundColor ms-duplex-aaa-bbb-string="xxx"//這個綁定屬性有三個參數,表示三種不一樣的攔截操做
若是一個頁面很是複雜,就須要劃分模塊,每一個模塊交由不一樣的ViewModel去處理。咱們就要用到ms-controller與ms-important來指定ViewModel了。
咱們看下面的例子:
<div ms-controller="AAA"> <div>{{name}} : {{color}}</div> <div ms-controller="BBB"> <div>{{name}} : {{color}}</div> <div ms-controller="CCC"> <div>{{name}} : {{color}}</div> </div> <div ms-important="DDD"> <div>{{name}} : {{color}}</div> </div> </div> </div>
avalon.ready(function() { avalon.define({ $id: "AAA", name: "liger", color: "green" }); avalon.define({ $id: "BBB", name: "sphinx", color: "red" }); avalon.define({ $id: "CCC", name: "dragon" //不存在color }); avalon.define({ $id: "DDD", name: "sirenia" //不存在color }); avalon.scan() })
能夠看出ViewModel在DOM樹的做用範圍其實與CSS很類似,採起就近原則,若是當前ViewModel沒有此字段 就找上一級ViewModel的同名字段,這個機制很是有利於團隊協做。
若是從另外一個角度來看,因爲這種隨機組成的方式就能實現相似繼承的方式,所以咱們就沒必要在JS代碼時構建複雜的繼承體系。
類的繼承體系是源自後端複雜業務的膨脹而誕生的。早在20世界80年代初期,也就是面向對象發展的初期,人們就很是看重繼承這個概念。 繼承關係蘊涵的意義是很是深遠的。使用繼承咱們能夠基於差別編程,也就是說,對於一個知足咱們大部分需求的類,能夠建立一個它的子類,重載它個別方法來實現咱們所要的功能。只子繼承一個類, 就能夠重類該類的代碼!經過繼承,咱們能夠創建完整的軟件結構分類,其中每個層均可以重用該層次以上的代碼。這是一個美麗新世界。
但類繼承的缺點也是很明顯的,在下摘錄一些:
面嚮對象語言與生俱來的問題就是它們與生俱來的這一整個隱性環境。你想要一根香蕉,但你獲得的是一頭手裏握着香蕉的大猩猩,以及整個叢林。 -- Joe Armstrong
在適合使用複合模式的共有類中使用繼承,會把這個類與它的超類永遠地束縛在一塊兒,從而人爲地限制了子類的性能
類繼承的缺點
- 超類改變,子類要跟着改變,違反了「開——閉」原則
- 不能動態改變方法實現,不能在運行時改變由父類繼承來的實現
- 破壞原有封裝,由於基類向子類暴露了實現細節
- 繼承會致使類的爆炸
所以在選擇是繼承仍是組合的問題上,avalon傾向組合。組合的使用範例就是CSS,所以也有了ms-important的誕生。
而ms-important就至關於CSS的important語句,強制這個區域使用此ViewModel,再也不往上查找同名屬性或方法!
另,爲了不未經處理的原始模板內容在頁面載入時在頁面中一閃而過,咱們可使用如下樣式(詳見這裏):
.ms-controller,.ms-important,[ms-controller],[ms-important]{ visibility: hidden; }
這是ms-skip負責。只要元素定義了這個屬性,不管它的值是什麼,它都不會掃描其餘屬性及它的子孫節點了。
<div ms-controller="test" ms-skip> <p ms-repeat-num="cd" ms-attr-name="num" ms-data-xxx="$index"> {{$index}} - {{num}} </p> A:<div ms-each="arr">{{yy}}</div> </div>
若是單是把DOM樹做爲一個模板遠遠不夠的,好比有幾個地方,須要重複利用一套HTML結構,這就要用到內部模板或外部模板了。
內部模板是,這個模板與目標節點是位於同一個DOM樹中。咱們用一個MIME不明的script標籤或者noscript標籤(0.94後支持,建議使用它)保存它,而後經過ms-include="id"引用它。
<html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <script src="avalon.js"></script> <script> avalon.define({ $id: "test", xxx: "引入內部模板" }) </script> </head> <body > <script type="avalon" id="tpl"> here, {{ 3 + 6 * 5 }} </script> <div ms-controller="test"> <p>{{xxx}}</p> <div ms-include="'tpl'"></div> </div> </body> </html>
注意,ms-include的值要用引號括起,表示這只是一個字符串,這時它就會搜索頁面的具備此ID的節點,取其innerHTML,放進ms-include所在的元素內部。不然這個tpl會被當成一個變量, 框架就會在VM中檢測有沒有此屬性,有就取其值,重複上面的步驟。若是成功,頁面會出現here, 2的字樣。
若是你們想在模板加載後,加工一下模板,可使用data-include-loaded來指定回調的名字。
若是你們想在模板掃描後,隱藏loading什麼的,可使用data-include-rendered來指定回調的名字。
因爲ms-include綁定須要定義在一個元素節點上,它的做用僅僅是一個佔位符,提供一個插入位置的容器。 若是用戶想在插入內容後,去掉這容器,可使用data-include-replace="true"。
下面是它們的實現
var vmodels = data.vmodels var rendered = getBindingCallback(elem.getAttribute("data-include-rendered"), vmodels) var loaded = getBindingCallback(elem.getAttribute("data-include-loaded"), vmodels) function scanTemplate(text) { if (loaded) { text = loaded.apply(elem, [text].concat(vmodels)) } avalon.innerHTML(elem, text) scanNodes(elem, vmodels) rendered && checkScan(elem, function() { rendered.call(elem) }) }
外部模板,一般用於多個頁面的複用,所以須要整成一個獨立的文件。這時咱們就須要經過ms-include-src="src"進行加載。
好比有一個HTML文件tmpl.html,它的內容爲:
<div>這是一個獨立的頁面</div> <div>它是經過AJAX的GET請求加載下來的</div>
而後咱們這樣引入它
<div ms-include-src="'tmpl.html'"></div>
有關它的高級應用的例子可見這裏利用ms-include與監控數組實現一個樹
注意,ms-include-src須要後端服務器支持,由於用到同域的AJAX請求。
這分兩種:文本綁定與HTML綁定,每種都有兩個實現方式
<script> avalon.define({ $id: "test", text: "<b> 1111 </b>" }) </script> <div ms-controller="test"> <div><em>用於測試是否被測除</em>xxxx{{text}}yyyy</div> <div><em>用於測試是否被測除</em>xxxx{{text|html}}yyyy</div> <div ms-text="text"><em>用於測試是否被測除</em>xxxx yyyy</div> <div ms-html="text"><em>用於測試是否被測除</em>xxxx yyyy</div> </div>
默認狀況下,咱們是使用{{ }} 進行插值,若是有特殊需求,咱們還能夠配置它們:
avalon.config({ interpolate:["<%","%>"] })
注意,你們不要用<, > 做爲插值表達式的界定符,由於在IE6-9裏可能轉換爲註釋節點,詳見這裏
插值表達式{{}}在綁定屬性的使用,只限那些能返回字符串的綁定屬性,如ms-attr、ms-css、ms-include、ms-class、 ms-href、 ms-title、ms-src等。一旦出現插值表達式,說明這個整個東西分紅可變的部分與不可變的部分,{{}}內爲可變的,反之亦然。 若是沒有{{}}說明整個東西都要求值,又如ms-include="'id'",要用兩種引號強制讓它的內部不是一個變量。
avalon提供了多種方式來綁定類名,有ms-class, ms-hover, ms-active, 具體可看這裏
avalon經過ms-on-click或ms-click進行事件綁定,並在IE對事件對象進行修復,具體可看這裏
avalon並無像jQuery設計一個近九百行的事件系統,連事件回調的執行順序都進行修復(IE6-8,attachEvent添加的回調在執行時並無按先入先出的順序執行),只是很薄的一層封裝,所以性能很強。
<!DOCTYPE HTML> <html> <head> <meta charset="UTF-8"> <title>有關事件回調傳參</title> <script src="avalon.js" type="text/javascript"></script> <script> avalon.ready(function() { avalon.define({ $id: "simple", firstName: "司徒", lastName: "正美", array: ["aaa", "bbb", "ccc"], argsClick: function(e, a, b) { alert(a+ " "+b) }, loopClick: function(a) { alert(a) } }); avalon.scan(); }) </script> </head> <body> <fieldset ms-controller="simple"> <legend>例子</legend> <div ms-click="argsClick($event, 100, firstName)">點我</div> <div ms-each-el="array" > <p ms-click="loopClick(el)">{{el}}</p> </div> </fieldset> </body> </html>
另外,這裏有一些結合ms-data實現事件代理的技巧,建議事件綁定接口支持事件代理,最簡單就是table上能夠綁定td的點擊事件
avalon經過ms-visible="bool"實現對某個元素顯示隱藏控制,它用是style.display="none"進行隱藏。
這個功能是抄自knockout的,ms-if="bool",一樣隱藏,但它是將元素移出DOM。這個功能直接影響到CSS :empty僞類的渲染結果,所以比較有用。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>ms-if</title> <script t src="avalon.js"></script> </head> <body ms-controller="test"> <ul ms-each-item="array"> <li ms-click="$remove" ms-if="$index % 2 == 0">{{ item }} --- {{$index}}</li> </ul> <script type="text/javascript"> avalon.define({ $id: "test", array: "a,b,c,d,e,f,g".split(",") }); </script> </body> </html>
這裏得介紹一下avalon的掃描順序,由於一個元素可能會存在多個屬性。總的流程是這樣的:
ms-skip --> ms-important --> ms-controller --> ms-if --> ms-repeat --> ms-if-loop --> ...-->ms-each --> ms-with --> ms-duplex
首先跑在最前面的是 ms-skip,只要元素定義了這個屬性,不管它的值是什麼,它都不會掃描其餘屬性及它的子孫節點了。而後是 ms-important, ms-controller這兩個用於圈定VM的做用域的綁定屬性,它們的值爲VM的$id,它們不會影響avalon繼續掃描。接着是ms-if,因爲一個頁面可能被當成子模塊,被不一樣的VM所做用,那麼就會出現有的VM沒有某個屬性的狀況。好比下面的狀況:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>ms-if</title> <script src="avalon.js"></script> </head> <body ms-controller="Test"> <h1>{{aaa}}</h1> <ul ms-if="array" ms-each-item="array"> <li ms-click="$remove" ms-if="$index % 2 == 0">{{ item }} --- {{$index}}</li> </ul> <script type="text/javascript"> avalon.define('Test', function(vm) { vm.aaa = "array不存在啊" }); </script> </body> </html>
若是沒有ms-if作代碼防護,確定報一大堆錯。
接着是 ms-repeat綁定。出於某些緣由,咱們不想顯示數組中的某些元素,就須要讓ms-if拖延到它們以後才起做用,這時就要用到ms-if-loop。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>ms-if</title> <script src="avalon.js"></script> </head> <body ms-controller="test"> <h1>{{aaa}}</h1> <ul> <li ms-repeat="array" ms-if-loop="el">{{ el }}</li> <li>它總在最後</li> </ul> <script type="text/javascript"> avalon.define({ $id: "test", array: ["aaa", "bbb", null, "ccc"] }); </script> </body> </html>
以後就是其餘綁定,但殿後的老是ms-duplex。從ms-if-loop到ms-duplex之間的執行順序是按這些綁定屬性的首字母的小寫的ASCII碼進行排序,好比同時存在ms-attr與ms-visible綁定,那麼先執行ms-attr綁定。若是咱們想綁定多個類名,用到ms-class, ms-class-2, ms-class-3, ms-class-1,那麼執行順序爲ms-class, ms-class-1, ms-class-2, ms-class-3。若是咱們要用到綁定多個點擊事件,須要這樣綁定:ms-click, ms-click-1, ms-click-2……更具體能夠查看源碼中的scanTag, scanAttr方法。
這功能抄自angular,原名ms-model起不得太好,姑且認爲利用VM中的某些屬性對錶單元素進行雙向綁定。
這個綁定,它除了負責將VM中對應的值放到表單元素的value中,還對元素偷偷綁定一些事件,用於監聽用戶的輸入從而自動刷新VM。
對於select type=multiple與checkbox等表示一組的元素, 須要對應一個數組;其餘表單元素則須要對應一個簡單的數據類型;若是你就是想表示一個開關,那大家能夠在radio, checkbox上使用ms-duplex-checked,須要對應一個布爾(在1.3.6以前的版本,radio則須要使用ms-duplex, checkbox使用ms-duplex-radio來對應一個布爾)。
新 | 舊(1.3.6以前) | 功能 |
ms-duplex-checked 只能應用於radio、 checkbox |
ms-duplex 只能應用於radio ms-duplex-radio checkbox 多用於實現GRID中的全選/全不選功能 |
經過checked屬性同步VM |
ms-duplex-string 應用於全部表單元素 |
ms-duplex-text 只能應用於radio |
經過value屬性同步VM |
ms-duplex-boolean 應用於全部表單元素 |
ms-duplex-bool 只能應用於radio |
value爲」true」時轉爲true,其餘值轉爲false同步VM |
ms-duplex-number 應用於表單元素 |
沒有對應項 | 若是value是數字格式就轉換爲數值,不然不作轉換,而後再同步VM |
ms-duplex 至關於ms-duplex-string |
ms-duplex 在radio至關於ms-duplex-checked 在其餘上至關於ms-duplex-string |
見上 |
注意:ms-duplex與ms-checked不能在同時使用於一個元素節點上。
注意:若是表單元素同時綁定了ms-duplex=xxx與ms-click或ms-change,而事件回調要當即獲得這個vm.xxx的值,input[type=radio]是存在問題,它不能當即獲得當前值,而是以前的值,須要在回調裏面加個setTimeout。
有關ms-duplex的詳細用法,你們能夠經過這個頁面進行學習。
<!DOCTYPE html> <html> <head> <title>ms-duplex</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> <div ms-controller="box"> <ul> <li><input type="checkbox" ms-click="checkAll" ms-checked="checkAllbool"/>全選</li> <li ms-repeat="arr" ><input type="checkbox" ms-value="el" ms-duplex="selected"/>{{el}}</li> </ul> </div> <script src="avalon.js" ></script> <script> var vm = avalon.define({ $id: "box", arr : ["1", '2', "3", "4"], selected : ["2", "3"], checkAllbool : false, checkAll : function() { if (this.checked) { vm.selected = vm.arr } else { vm.selected.clear() } } }) vm.checkAllbool = vm.arr.length === vm.selected.length vm.selected.$watch("length", function(n) { vm.checkAllbool = n === vm.arr.size() }) </script> </body> </html>
對於非radio, checkbox, select的控件,咱們能夠經過data-duplex-changed來指定一個回調,傳參爲元素的value值,this指向元素自己,要求必須有返回值。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>data-duplex-changed</title> <script src="avalon.js"></script> </head> <body ms-controller="duplex"> <input ms-duplex="username" data-duplex-changed="callback"> <script type="text/javascript"> avalon.define({ $id: "duplex", username : "司徒正美", callback : function(val){ avalon.log(val) avalon.log(this) return this.value = val.slice(0, 10)//不能超過10個字符串 } }); </script> </body> </html>
用法爲ms-css-name="value"
注意:屬性值不能加入CSS hack與important!
<!DOCTYPE html> <html> <head> <title>by 司徒正美</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script src="../avalon.js"></script> <script> avalon.define({ $id: "test", o: 0.5, bg: "#F3F"// 不能使用CSS hack,如 bg : "#F3F\9\0" }) </script> <style> .outer{ width:200px; height: 200px; position: absolute; top:1px; left:1px; background: red; z-index:1; } .inner{ width:100px; height: 100px; position: relative; top:20px; left:20px; background: green; } </style> </head> <body ms-controller="test" > <h3>在舊式IE下,若是父元素是定位元素,但沒有設置它的top, left, z-index,那麼爲它設置透明時, 它的全部被定位的後代都沒有透明</h3> <div class="outer" ms-css-opacity="o" ms-css-background-color="bg" > <div class="inner"></div> </div> </body> </html>
用法爲ms-data-name="value", 用於爲元素節點綁定HTML5 data-*屬性。
這主要涉及到表單元素幾個很是重要的布爾屬性,即disabed, readyOnly, selected , checked, 分別使用ms-disabled, ms-enabled, ms-readonly, ms-checked, ms-selected。ms-disabled與ms-enabled是對立的,一個true爲添加屬性,另外一個true爲移除屬性。
這主要涉及到幾個很是經常使用的字符串屬性,即href, src, alt, title, value, 分別使用ms-href, ms-src, ms-alt, ms-title, ms-value。它們的值的解析狀況與其餘綁定不同,若是值沒有{{}}插值表達式,那麼就當成VM中的一個屬性,而且能夠與加號,減號混用, 組成表達式,若是裏面有表達式,整個當成一個字符串。
<a ms-href="aaa + '.html'">xxxx</a> <a ms-href="{{aaa}}.html">xxxx</a>
ms-attr-name="value",這個容許咱們在元素上綁定更多種類的屬性,如className, tabIndex, name, colSpan什麼的。
用法爲ms-repeat-xxx="array", 其中xxx能夠隨意命名(注意,不能出現大寫,由於屬性名在HTML規範中,會所有轉換爲小寫,詳見這裏),如item, el。 array對應VM中的一個普通數組或一個監控數組。監控數組擁有原生數組的全部方法,而且比它還多了set, remove, removeAt, removeAll, ensure, pushArray與 clear方法 。詳見這裏。
在早期,avalon提供了一個功能類似的ms-each綁定。ms-each與ms-repeat的不一樣之處在於,前者循環它的孩子(如下圖爲例,可能包含LI元素兩邊的空白),後者循環它自身。
注意,ms-each, ms-repeat會生成一個新的代理VM對象放進當前的vmodels的前面,這個代理對象擁有el, $index, $first, $last, $remove, $outer等屬性。另外一個會產生VM對象的綁定是ms-widget。
咱們還能夠經過data-repeat-rendered, data-each-rendered來指定這些元素都插入DOM被渲染了後執行的回調,this指向元素節點, 有一個參數表示爲當前的操做,是add, del, move, index仍是clear
vm.array = [1,2,3] vm.rendered = function(action){ if(action === "add"){ avalon.log("渲染完畢")//注意,咱們經過vm.array.push(4,5)添加元素,會連續兩次觸發rendered,第一次add,第二次爲index } } <li data-repeat-rendered="rendered" ms-repeat="array">{{el}}</li>
<!DOCTYPE html> <html> <head> <title></title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script src="avalon.js"></script> <style> .id2013716 { width: 200px; float:left; } </style> <script> var a = avalon.define({ $id: "array", array: ["1", "2", "3", "4"] }) setTimeout(function() { a.array.set(0, 7) }, 1000); var b = avalon.define({ $id: "complex", array: [{name: "xxx", sex: "aaa", c: {number: 2}}, {name: "yyy", sex: "bbb", c: {number: 4}}]// }); setTimeout(function() { b.array[0].c.number = 9 b.array[0].name = "1000" }, 1000) setTimeout(function() { a.array.push(5, 6, 7, 8, 9) }, 1000) setTimeout(function() { a.array.unshift("a", "b", "c", "d") }, 2000) setTimeout(function() { a.array.shift() b.array[1].name = 7 }, 3000) setTimeout(function() { a.array.pop() }, 4000) setTimeout(function() { a.array.splice(1, 3, "x", "y", "z") b.array[1].name = "5000" }, 5000) </script> </head> <body> <fieldset class="id2013716" ms-controller="array"> <legend>例子</legend> <ul ms-each="array"> <li >數組的第{{$index+1}}個元素爲{{el}}</li> </ul> <p>size: <b style="color:red">{{array.size()}}</b></p> </fieldset> <fieldset class="id2013716" ms-controller="complex"> <legend>例子</legend> <ul > <li ms-repeat-el="array">{{el.name+" "+el.sex}}它的內容爲 number:{{el.c.number}}</li> </ul> </fieldset> </body> </html>
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <title></title> </head> <body ms-controller="page"> <h3>ms-each實現數組循環</h3> <div ms-each="arr"> {{$index}} <button ms-click="$remove">{{el}} 點我刪除</button> </div> <h3>ms-repeat實現數組循環</h3> <table border="1" width="800px" style="background:blueviolet"> <tr> <td ms-repeat="arr"> {{el}} {{$first}} {{$last}} </td> </tr> </table> <h3>ms-repeat實現數組循環</h3> <ul> <li ms-repeat="arr"><button ms-click="$remove">測試{{$index}}</button>{{el}}</li> </ul> <h3>ms-repeat實現對象循環</h3> <ol > <li ms-repeat="object">{{$key}}:{{$val}}</li> </ol> <h3>ms-with實現對象循環</h3> <ol ms-with="object"> <li>{{$key}}:{{$val}}</li> </ol> <h3>經過指定data-with-sorted規定只輸出某一部分建值及它們的順序,只能循環對象時有效</h3> <ol ms-with="bigobject" data-with-sorted="order" title='with'> <li>{{$key}}:{{$val}}</li> </ol> <ol title='repeat'> <li ms-repeat="bigobject" data-with-sorted="order">{{$key}}:{{$val}}</li> </ol> <h3>ms-repeat實現數組雙重循環</h3> <table border="1" style="background:yellow" width="400px"> <tr ms-repeat="dbarray"><td ms-repeat-elem="el.array">{{elem}}</td></tr> </table> <h3>ms-each實現數組雙重循環</h3> <table border="1" style="background:green" width="400px"> <tbody ms-each="dbarray"> <tr ms-each-elem="el.array"><td>{{elem}}</td></tr> </tbody> </table> <h3>ms-with實現對象雙重循環,並經過$outer訪問外面的鍵名</h3> <div ms-repeat="dbobjec">{{$key}}:<strong ms-repeat="$val">{{$key}} {{$val}} <span style="font-weight: normal">{{$outer.$key}}</span>| </strong></div> <script src="avalon.js"></script> <script> var model = avalon.define({ $id: "page", arr : ["a", "b", "c", "d", "e", "f", "g", "h"], object : { "kkk": "vvv", "kkk2": "vvv2", "kkk3": "vvv3" }, aaa : { aaa2: "vvv2", aaa21: "vvv21", aaa22: "vvv22" }, bigobject : { title: 'xxx', name: '777', width: 30, align: 'center', sortable: true, cols: "cols3", url: 'data/stockQuote.json', method: 'get', remoteSort: true, sortName: 'SECUCODE', sortStatus: 'asc' }, order : function() { return ["name", "sortStatus", "sortName", "method", "align"] }, dbobjec : { aaa: { aaa2: "vvv2", aaa21: "vvv21", aaa22: "vvv22" }, bbb: { bbb2: "ccc2", bbb21: "ccc21", bbb22: "ccc22" } }, dbarray : [ { array: ["a", "b", "c"] }, { array: ["e", "f", "d"] } ] }); setTimeout(function() { model.object = { a1: 4444, a2: 5555 } model.bigobject = { title: 'yyy', method: 'post', name: '999', width: 78, align: 'left', sortable: false, cols: "cols5", url: 'data/xxx.json', remoteSort: false, sortName: 'FAILURE', sortStatus: 'bbb' } }, 3000) </script> </body> </html>
語法與ms-repeat幾乎一致,建議用ms-repeat代替。
語法爲 ms-with="obj" 子元素裏面用$key, $val分別引用鍵名,鍵值。另咱們能夠經過指定data-with-sorted回調,規定只輸出某一部分建值及它們的順序。 注意,此綁定已經不建議使用,它將被ms-repeat代替,ms-repeat裏面也可使用data-with-sorted回調。
<!DOCTYPE html> <html> <head> <title></title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script type='text/javascript' src="avalon.js"></script> <script> var a = avalon.define({ $id: "xxx", obj: { aaa: "xxx", bbb: "yyy", ccc: "zzz" }, first: "司徒正美" }) setTimeout(function() { a.obj.aaa = "7777777777" a.first = "清風火忌" }, 1000) setTimeout(function() { a.obj.bbb = "8888888" }, 3000) </script> </head> <body ms-controller="xxx"> <div ms-with="obj"> <div>{{$key}} {{$val}}</div> </div> <hr/> <div ms-with="obj"> <div>{{$key}} {{$val}}</div> </div> <hr/> <div ms-with="obj"> <div>{{$key}} {{$val}}</div> </div> </body> </html>
有關ms-each, ms-repeat, ms-with更高的用法,如雙重循環什麼的,能夠看這裏
它的格式爲ms-widget="uiName, id?, optsName?"
下面是一個完整的實例用於教導你如何定義使用一個UI。
avalon內置了一個強大的自定義事件系統,它在綁定在每個VM上。每個VM都擁有$watch, $unwatch, $fire這三個方法,及一個$events對象。$events是用於儲存各類回調。先從單個VM提及,若是一個VM擁有aaa這個屬性,若是咱們在VM經過$watch對它監控,那麼當aaa改變值時,它對應的回調就會被觸發!
var vmodel = avalon.define({ $id: "test", aaa: 111 }) vmodel.$watch("aaa", function(newValue, oldValue){ avalon.log(newValue) //222 avalon.log(oldValue) //111 }) setTimeout(function(){ vmodel.aaa = 222 }, 1000)
注意,它只能監聽當前屬性的變更。
咱們還能夠經過$unwatch方法,移除對應的回調。若是傳入兩個參數,第一個是屬性名,第二個是回調,那麼只移除此回調;若是隻傳入一個屬性名,那麼此屬性關聯的全部回調都會被移除掉。
有時,咱們還綁定了一些與屬性名無關的事件回調,想觸發它,那隻能使用$fire方法了。$fire方法第一個參數爲屬性名(自定義事件名),其餘參數隨意。
var vmodel = avalon.define({ $id: "test", aaa: 111 }) vmodel.$watch("cat", function(){ avalon.log(avalon.slice(arguments)) //[1,2,3] }) setTimeout(function(){ vmodel.$fire("cat",1,2,3) }, 1000)
更高級的玩法,有時咱們想在任何屬性變化時都觸發某一個回調,這時咱們就須要$watch一個特殊的屬性了——「$all」。不一樣的是,$watch回調的參數多了一個屬性名,排在最前面。
var vmodel = avalon.define({ $id: "test", aaa: 111, bbb: 222, }) vmodel.$watch("$all", function(){ avalon.log(avalon.slice(arguments)) // ["aaa", 2, 111] // ["bbb", 3, 222] }) setTimeout(function(){ vmodel.aaa = 2 vmodel.bbb = 3 }, 1000)
手動觸發$fire是位隨着高風險的,框架內部是作了處理(只有先後值發生變化纔會觸發),所以萬不得已使用它,但又爆發死循環怎麼辦?這樣就須要暫時中斷VM的屬性監控機制。使用$unwatch(),它裏面什麼也不傳,就暫時將監控凍結了。恢復它也很簡單,使用$watch(),裏面也什麼也不傳!
不過最強大的用法是實現模塊間的通訊(由於在實際項目中,一個頁面可能很是大,有多少人分塊製做,每一個人本身寫本身的VM,這時就須要經過某種機制來進行數據與方法的聯動了),這是使用$fire方法達成的。只要在$fire的自定義事件名前添加"up!", "down!", "all!"前綴,它就能實現angular類似的$emit,$broadcast功能。
<!DOCTYPE html> <html> <head> <title>by 司徒正美</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script src="avalon.js"></script> <script> var vm1 = avalon.define({ $id: "ancestor", aaa : '1111111111', click : function() { avalon.log("向下廣播") vm1.$fire("down!aaa", "capture") } }) vm1.$watch("aaa", function(v) { avalon.log(v) avalon.log("ancestor.aaa事件被觸發了") }) var vm2 = avalon.define({ $id: "parent", text : "222222222" aaa : '3333333333', click : function() { console.log("全局擴播") vm2.$fire("all!aaa", "broadcast") } }) vm2.$watch("aaa", function(v) { avalon.log(v) avalon.log("parent.aaa事件被觸發了") }) var vm3 = avalon.define( $id: "son", click : function() { console.log("向上冒泡") vm3.$fire("up!aaa", "bubble") } }) vm3.$watch("aaa", function(v) { avalon.log(v) avalon.log("son.aaa事件被觸發了") }) </script> <style> </style> </head> <body class="ms-controller" ms-controller="ancestor"> <h3>avalon vm.$fire的升級版 </h3> <button type="button" ms-click="click"> capture </button> <div ms-controller="parent"> <button type="button" ms-click="click">broadcast</button> <div ms-controller="son"> <button type="button" ms-click="click"> bubble </button> </div> </div> </body> </html>
avalon從angular中抄來管道符風格的過濾器,但有點不同。 它只能用於{{}}插值表達式。若是不存在參數,要求直接跟|filter,若是存在參傳,則要用小括號括起,參數要有逗號,這與通常的函數調用差很少,如|truncate(20,"……")
avalon自帶如下幾個過濾器
decimals 可選,規定多少個小數位。 dec_point 可選,規定用做小數點的字符串(默認爲 . )。 thousands_sep 可選,規定用做千位分隔符的字符串(默認爲 , ),若是設置了該參數,那麼全部其餘參數都是必需的。
'yyyy': 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) 'yy': 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) 'y': 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) 'MMMM': Month in year (January-December) 'MMM': Month in year (Jan-Dec) 'MM': Month in year, padded (01-12) 'M': Month in year (1-12) 'dd': Day in month, padded (01-31) 'd': Day in month (1-31) 'EEEE': Day in Week,(Sunday-Saturday) 'EEE': Day in Week, (Sun-Sat) 'HH': Hour in day, padded (00-23) 'H': Hour in day (0-23) 'hh': Hour in am/pm, padded (01-12) 'h': Hour in am/pm, (1-12) 'mm': Minute in hour, padded (00-59) 'm': Minute in hour (0-59) 'ss': Second in minute, padded (00-59) 's': Second in minute (0-59) 'a': am/pm marker 'Z': 4 digit (+sign) representation of the timezone offset (-1200-+1200) format string can also be one of the following predefined localizable formats: 'medium': equivalent to 'MMM d, y h:mm:ss a' for en_US locale (e.g. Sep 3, 2010 12:05:08 pm) 'short': equivalent to 'M/d/yy h:mm a' for en_US locale (e.g. 9/3/10 12:05 pm) 'fullDate': equivalent to 'EEEE, MMMM d,y' for en_US locale (e.g. Friday, September 3, 2010) 'longDate': equivalent to 'MMMM d, y' for en_US locale (e.g. September 3, 2010 'mediumDate': equivalent to 'MMM d, y' for en_US locale (e.g. Sep 3, 2010) 'shortDate': equivalent to 'M/d/yy' for en_US locale (e.g. 9/3/10) 'mediumTime': equivalent to 'h:mm:ss a' for en_US locale (e.g. 12:05:08 pm) 'shortTime': equivalent to 'h:mm a' for en_US locale (e.g. 12:05 pm)
例子:
生成於{{ new Date | date("yyyy MM dd:HH:mm:ss")}}
生成於{{ "2011/07/08" | date("yyyy MM dd:HH:mm:ss")}}
生成於{{ "2011-07-08" | date("yyyy MM dd:HH:mm:ss")}}
生成於{{ "01-01-2000" | date("yyyy MM dd:HH:mm:ss")}}
生成於{{ "03 04,2000" | date("yyyy MM dd:HH:mm:ss")}}
生成於{{ "3 4,2000" | date("yyyy MM dd:HH:mm:ss")}}
生成於{{ 1373021259229 | date("yyyy MM dd:HH:mm:ss")}}
生成於{{ "1373021259229" | date("yyyy MM dd:HH:mm:ss")}}
值得注意的是,new Date可傳的格式類型很是多,但不是全部瀏覽器都支持這麼多,詳看這裏
多個過濾器一塊兒工做
<div>{{ prop | filter1 | filter2 | filter3(args, args2) | filter4(args)}}</div>
若是想自定義過濾器,能夠這樣作
avalon.filters.myfilter = function(str, args, args2){//str爲管道符以前計算獲得的結果,默認框架會幫你傳入,此方法必須返回一個值 /* 具體邏輯 */ return ret; }
avalon裝備了AMD模範的加載器,這涉及到兩個全局方法 require與define
require(deps, callback)
deps 必需。String|Array。依賴列表,能夠是具體路徑或模塊標識,若是想用字符串表示多個模塊,則請用「,」隔開它們。
callback 必需。Function。回調,當用戶指定的依賴以及這些依賴的依賴樹都加載執行完畢後,纔會安全執行它。
若是想禁止使用avalon自帶的加載器,能夠在第一次調用require方法以前,執行以下代碼:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script src="require.js"></script> <script src="avalon.modern.js"></script> <script> avalon.config({loader: false}) alert(require) avalon.define("xxx", function(vm){ vm.aaa = "司徒正美" }) </script> </head> <body ms-controller="xxx" > <div>{{aaa}}</div> </body> </html>
與jquery更好的集成,好比一些舊系統,直接在頁面引入jquery庫與其大量jquery插件,改爲動態加載方式成本很是大。怎麼樣才能與jquery和平共存,亦能讓AMD加載發揮做呢?先引入jquery庫, 而後將avalon.modules.jquery 加個預設值(exports: jquery用於shim機制, state: 2 代表它已經加載完畢)就好了。
若是你想用其餘AMD加載器,最好的辦法仍是建議直接打開源碼,拉到最底幾行,把加載器禁用了!
avalon.config({ loader: false//原來是true!!!!!!!!!!1 })
define方法用於定義一個模塊,格式爲:
define( id?, deps?, factory )
id 可選。String。模塊ID。它最終會轉換一個URL,放於 $.modules中。 deps 可選。String|Array。依賴列表。 factory 必需。Function|Object。模塊工廠。它的參數列參爲其依賴模塊全部返回的值,若是某個模塊沒有返回值,則對應位置爲undefined
注意, define方法不能寫在script標籤的innerHTML中,只能寫在JS文件裏。
avalon與seajs, https://github.com/RubyLouvre/avalon/issues/313
咱們也能夠在源碼裏面直接移除AMD加載器模塊。
它須要依賴於另外一個獨立的組件mmRouter,用法請見這裏
AJAX可使用jQuery或mmRequest, mmRequest體積更少,覆蓋jQuery ajax模塊的90%功能,而且在現代瀏覽器中使用了XMLHttpRequest2實現,性能更佳。
經過AJAX加載新數據到已存在的VM中
$.ajax({ url: url, data: JSON.parse(JSON.stringify(vm.$model)), //去掉數據模型中的全部函數 success: function(ajaxData) { //須要本身在這裏定義一個函數,將缺乏的屬性補上,無用的數據去掉, //格式不正確的數據轉換好 ajaxData最後必須爲一個對象 ajaxData = filterData(ajaxData) //先已有的數據,新的數據,所有拷貝到一個全新的空對象中,再賦值,防止影響原來的$model var newData = avalon.mix(true, {}, vm.$model, ajaxData) for (var i in newData) { if (vm.hasOwnProperty(i) && i !== "hasOwnProperty"){//安全更新數據 vm[i] = newData[i] } } } })
提交VM中的數據到後臺,要當心死循環,詳見這裏
文件上傳要用mmRequest的upload方法
avalon如今有三個擴展點,一是在avalon.fn上添加新的原型方法,這是用於處理DOM的,二是在avalon.bindingHandlers與 avalon.bindingExecutors上添加新的綁定(ms-xxx),三是在avalon.filters添加新的過濾器。
添加原型方法就不用多說,建議儘量返回this,實現鏈式操做,this[0]爲它包含的元素節點。
添加過濾器也很簡,翻看源碼看看lowercase如何實現就好了。
添加新綁定難一點,框架bindingHandlers要求對應的處理函數有兩個參數,data與vmodels, data擁有以下幾個屬性:
vmodels是指,從DOM樹最頂點到添加此綁定的元素所路過的ms-controller的值(它們都對應一個VM)。注意,ms-each, ms-with也產生VM。
bindingHandlers裏的函數用於初始化綁定,它會對綁定屬性作一些分解,放進parseExprProxy中,parseExprProxy會再調用parseExpr,將它轉換爲求值函數,放進行對應VM屬性的subscribers數組內(操做方爲registerSubscriber)。
bindingExecutors裏的的函數爲真正的視圖刷新函數,每當VM發生改變後,都會被執行(操做方爲notifySubscribers)。
如今avalon擁有如此多綁定:
因爲IE6下沒有console.log,若是又不想用VS等巨無霸IDE,能夠本身定義如下方法
if(!window.console){ window.console = {} console.log = function(str){ avalon.ready(function() { var div = document.createElement("pre"); div.className = "mass_sys_log"; div.innerHTML = str + ""; //確保爲字符串 document.body.appendChild(div); }); } }
上線後,將.mass_sys_log{ display: none; }
若是是高級瀏覽器,avalon會在控制檯上打印許多調試消息,若是不想看到它們,能夠這樣屏蔽它們:avalon.config({debug: false})
將頁面模塊化,大量使用ms-include-src,沒有權限就返回空頁面,權限夠了,但不是最高級,那它返回的模板文件也不同/p>
利用avalon 實現一個簡單的成績單, 教你如何使用ms-each數組循環綁定與$watch回調