關於AvalonJS
avalon是一個簡單易用迷你的MVVM框架,它最早發佈於2012.09.15,爲解決同一業務邏輯存在各種視圖呈現而開發出來的。 事實上,這問題其實也可以簡單地利用一般的前端模板加jQuery 事件委託 搞定,但隨着業務的膨脹, 代碼就充滿了各種選擇器與事件回調,難以維護。因此徹底的將業務與邏輯分離,就只能求助於架構。 最初想到的是MVC,嘗試過backbone,但代碼不降反升,很偶爾的機會,碰上微軟的WPF,
優雅的MVVM架構立即吸引住我,我覺得這就是我一直追求的解決之道。
MVVM將所有前端代碼徹底分成兩部分,視圖的處理通過綁定實現(angular有個更炫酷的名詞叫指令), 業務邏輯則集中在一個個叫VM的對象中處理。我們只要操作VM的數據,它就自然而然地神奇地同步到視圖。 顯然所有神祕都有其內幕,C#是通過一種叫訪問器屬性的語句實現,那麼JS也有沒有對應的東西。 感謝上帝,IE8最早引入這東西(Object.defineProperty),可惜有BUG,但帶動了其他瀏覽器實現它,IE9+便能安全使用它。 對於老式IE,我找了好久,實在沒有辦法,使用VBScript實現了。
Object.defineProperty或VBS的作用是將對象的某一個屬性,轉換一個setter與getter, 我們只要劫持這兩個方法,通過Pub/Sub模式就能偷偷操作視圖。爲了紀念WPF的指引,我將此項目以WPF最初的開發代號avalon來命名。 它真的能讓前端人員脫離DOM的苦海,來到數據的樂園中!
優勢
絕對的優勢就是降低了耦合, 讓開發者從複雜的各種事件中掙脫出來。 舉一個簡單地例子, 同一個狀態可能跟若干個事件的發生順序與發生時的附加參數都有關係, 不用 MVC (包括 MVVM) 的情況下, 邏輯可能非常複雜而且脆弱。 並且通常需要在不同的地方維護相關度非常高的一些邏輯, 稍有疏忽就會釀成 bug 不能自拔。使用這類框架能從根本上降低應用開發的邏輯難度, 並且讓應用更穩健。
除此之外, 也免去了一些重複的體力勞動, 一個 {value} 就代替了一行 $(selector).text(value)。 一些個常用的 directive 也能快速實現一些原本可能需要較多代碼才能實現的功能
- 使用簡單,作者是吃透了knockout, angular,rivets API設計出來,沒有太多複雜的概念, 指令數量控制得當,基本能覆蓋所有jQuery操作, 確保中小型公司的菜鳥前端與剛轉行過來的後端也能迅速上手。
- 兼容性非常好, 支持IE6+,firefox3.5+, opera11+, safari5+, chrome4, 最近也將國產的山寨瀏覽器(360, QQ, 搜狗,獵豹, 邀遊等)加入兼容列隊 (相比其他MVVM框架,KnockoutJS(IE6), AngularJS1.3(IE9), EmberJS(IE8), WinJS(IE9))
- 向前兼容非常好,不會出現angular那種跳崖式升級
- 注重性能,由於avalon一直在那些上千的大表格里打滾,經歷長期的優化, 它能支撐14000以上綁定(相對而言,angular一個頁面只能放2000個綁定)。另,在IE10等能良好支持HTML5的瀏覽器, 還提供了avalon.modern.js這個高性能的版本。
- 沒有任何依賴,不到5000行,壓縮後不到50KB
- 完善的單元測試,由於測試代碼非常龐大,放在獨立的倉庫中——
avalon.test
- 擁有一個包含2個Grid,1個樹,1 個驗證插件等總數近50個UI組件庫
OniUI, 由去哪兒網前端架構組在全力開發與維護
- 存在一個活躍的小社區,由於國內已有不少公司在用,我們都集中一個QQ羣裏互相交流幫助 QQ:79641290、228372837(註明來學avalon)
- 支持管道符風格的過濾函數,方便格式化輸出
- 讓DOM操作的代碼近乎絕跡,因此實現一個功能,大致把比jQuery所寫的還要少50%
- 使用類似CSS的重疊覆蓋機制,讓各個ViewModel分區交替地渲染頁面
- 節點移除時,智能卸載對應的視圖刷新函數,節約內存
- 操作數據即操作DOM,對ViewModel的操作都會同步到View與Model去
- 自帶AMD模塊加載器,省得與其他加載器進行整合
avalon現在有三個分支:avalon.js 兼容IE6,標準瀏覽器, 及主流山寨瀏覽器(QQ, 獵豹, 搜狗, 360, 傲遊); avalon.modern.js 則只支持IE10等支持HTML5現代瀏覽器 ; avalon.mobile.js,添加了觸屏事件與fastclick支持,用於移動端
開始的例子
我們從一個完整的例子開始認識 avalon :
< meta
http-equiv = "Content-Type"
content = "text/html; charset=UTF-8" > |
< script
src = "avalon.js" ></ script > |
< div
ms-controller = "box" > |
< div
style = " background: #a9ea00;"
ms-css-width = "w"
ms-css-height = "h"
ms-click = "click" ></ div > |
< p >W: < input
type = "text"
ms-duplex = "w"
data-duplex-event = "change" /></ p > |
< p >H: < input
type = "text"
ms-duplex = "h"
/></ p > |
vm.w = parseFloat(vm.w) + 10; |
vm.h = parseFloat(vm.h) + 10; |
上面的代碼中,我們可以看到在JS中,沒有任何一行操作DOM的代碼,也沒有選擇器,非常乾淨。在HTML中, 我們發現就是多了一些以ms-開始的屬性與{{}}標記,有的是用於渲染樣式, 有的是用於綁定事件。這些屬性或標記,實質就是avalon的綁定系統的一部分。綁定(有的框架也將之稱爲指令), 負責幫我們完成視圖的各種操作,相當於一個隱形的jQuery。正因爲有了綁定,我們就可以在JS代碼專注業務邏輯本身, 寫得更易維護的代碼!
掃描
不過上面的代碼並不完整,它能工作,是因爲框架默認會在DOMReady時掃描DOM樹,將視圖中的綁定屬性與{{}}插值表達式抽取出來,轉換爲求值函數與視圖刷新函數。
上面的JS代碼相當於:
avalon.ready( function () { |
vm.w = parseFloat(vm.w) + 10; |
vm.h = parseFloat(vm.h) + 10; |
avalon.scan是一個非常重要的方法,它有兩個可選參數,第一個是掃描的起點元素,默認是HTML標籤,第2個是VM對象。
avalon.scan =
function (elem, vmodel) { |
var
vmodels = vmodel ? [].concat(vmodel) : [] |
視圖模型
視圖模型,ViewModel,也經常被略寫成VM,是通過avalon.define方法進行定義。生成的對象會默認放到avalon.vmodels對象上。 每個VM在定義時必須指定$id。如果你有某些屬性不想監聽,可以直接將此屬性名放到$skipArray數組中。
var
array = (val || "" ).split( " " ); |
this .firstName = array[0] ||
"" ; |
this .lastName = array[1] ||
"" ; |
return
this .firstName + " "
+ this .lastName; |
接着我們說一些重要的概念:
- $id, 每個VM都有$id,如果VM的某一個屬性是對象(並且它是可監控的),也會轉換爲一個VM,這個子VM也會默認加上一個$id。 但只有用戶添加的那個最外面的$id會註冊到avalon.vmodels對象上。
- 監控屬性,一般地,VM中的屬性都會轉換爲此種屬性,當我們以vm.aaa = yyy這種形式更改其值時,就會同步到視圖上的對應位置上。
- 計算屬性,定義時爲一個對象,並且只存在set,get兩個函數或只有一個get一個函數。它是監控屬性的高級形式,表示它的值是通過函數計算出來的,是依賴於其他屬性合成出來的。
- 監控數組,定義時爲一個數組,它會添加了許多新方法,但一般情況下與普通數組無異,但調用它的push, unshift, remove, pop等方法會同步視圖。
- 非監控屬性,這包括框架添加的$id屬性,以$開頭的屬性,放在$skipArray數組中的屬性,值爲函數、元素節點、文本節點的屬性,總之,改變它們的值不會產生同步視圖的效果。
$skipArray 是一個字符串數組,只能放當前對象的直接屬性名,想禁止子對象的某個屬性的監聽,在那個子對象上再添加一個$skipAray數組就行了。
視圖裏面,我們可以使用ms-controller, ms-important指定一個VM的作用域。
此外,在ms-each, ms-with,ms-repeat綁定屬性中,它們會創建一個臨時的VM,我們稱之爲代理VM, 用於放置$key, $val, $index, $last, $first, $remove等變量或方法。
另外,avalon不允許在VM定義之後,再追加新屬性與方法,比如下面的方式是錯誤的:
我們再看看如何更新VM中的屬性(重點):
var model : avalon.define({ |
simpleArray : [1, 2, 3, 4], |
objectArray : [{name: "a"}, {name: "b"}, {name: "c"}, {name: "d"}], |
simpleArray : [1, 2, 3, 4], |
objectArray : [{name: "a", value: "aa"}, {name: "b", value: "bb"}, {name: "c", value: "cc"}, {name: "d", value: "dd"}], |
//如果是更新簡單數據類型(string, boolean, number)或Date類型 |
model.time = new Date(date.setFullYear(2005)) |
//只能全是字符串,或是全是布爾,不能有一些是這種類型,另一些是其他類型 |
//這時我們可以使用set方法來更新(它有兩個參數,第一個是index,第2個是新值) |
model.simpleArray.set(0, 1000) |
model.simpleArray.set(2, 3000) |
model.objectArray.set(0, {name: "xxxxxxxxxxxxxxxx", value: "xxx"}) |
model.objectArray[1].name = "5555" |
//如果要更新對象,直接賦給它一個對象,注意不能將一個VM賦給它,可以到VM的$model賦給它(要不會在IE6-8中報錯) |
< div
ms-controller = "update" > |
< div >{{time | date("yyyy - MM - dd mm:ss")}}</ div > |
< ul
ms-each = "simpleArray" > |
< div > < select
ms-each = "objectArray" > |
< option
ms-value = "el.value" >{{el.name}}</ option > |
< li >{{$key}} {{$val}}</ li > |
這裏還有個例子,大家認真看看。
綁定
avalon的綁定(或指令),擁有以下三種類型:
- {{}}插值表達式, 這是開標籤與閉標籤間,換言之,也是位於文本節點中,innerText裏。{{}}裏面可以添加各種過濾器(以|進行標識)。值得注意的是{{}}實際是文本綁定(ms-text)的一種形式。
- ms-*綁定屬性, 這是位於開標籤的內部, 95%的綁定都以這種形式存在。 它們的格式大概是這樣劃分的"ms" + type + "-" + param1 + "-" + param1 + "-" + param2 + ... + number = value
ms-attr-src= "xxx/{{a}}/yyy/{{b}}"
|
ms-class-1= "xxx"
ms-class-2= "yyy"
ms-class-3= "xxx" |
ms-css-background-color= "xxx"
|
ms-duplex-aaa-bbb-string= "xxx" |
- data-xxx-yyy="xxx",輔助指令,比如ms-duplex的某一個輔助指令爲data-duplex-event="change",ms-repeat的某一個輔助指令爲data-repeat-rendered="yyy"
作用域綁定(ms-controller, ms-important)
如果一個頁面非常複雜,就需要劃分模塊,每個模塊交由不同的ViewModel去處理。我們就要用到ms-controller與ms-important來指定ViewModel了。
我們看下面的例子:
HTML結構
< 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 >{{name}} : {{color}}</ div > |
ViewModel
avalon.ready(function() { |
name: "dragon" //不存在color |
name: "sirenia" //不存在color |
可以看出ViewModel在DOM樹的作用範圍其實與CSS很相似,採取就近原則,如果當前ViewModel沒有此字段 就找上一級ViewModel的同名字段,這個機制非常有利於團隊協作。
如果從另一個角度來看,由於這種隨機組成的方式就能實現類似繼承的方式,因此我們就不必在JS代碼時構建複雜的繼承體系。
類的繼承體系是源自後端複雜業務的膨脹而誕生的。早在20世界80年代初期,也就是面向對象發展的初期,人們就非常看重繼承這個概念。 繼承關係蘊涵的意義是非常深遠的。使用繼承我們可以基於差異編程,也就是說,對於一個滿足我們大部分需求的類,可以創建一個它的子類,重載它個別方法來實現我們所要的功能。只子繼承一個類, 就可以重類該類的代碼!通過繼承,我們可以建立完整的軟件結構分類,其中每一個層都可以重用該層次以上的代碼。這是一個美麗新世界。
但類繼承的缺點也是很明顯的,在下摘錄一些:
面嚮對象語言與生俱來的問題就是它們與生俱來的這一整個隱性環境。你想要一根香蕉,但你得到的是一頭手裏握着香蕉的大猩猩,以及整個叢林。 -- Joe Armstrong
在適合使用複合模式的共有類中使用繼承,會把這個類與它的超類永遠地束縛在一起,從而人爲地限制了子類的性能
類繼承的缺點
- 超類改變,子類要跟着改變,違反了“開——閉”原則
- 不能動態改變方法實現,不能在運行時改變由父類繼承來的實現
- 破壞原有封裝,因爲基類向子類暴露了實現細節
- 繼承會導致類的爆炸
因此在選擇是繼承還是組合的問題上,avalon傾向組合。組合的使用範例就是CSS,因此也有了ms-important的誕生。
而ms-important就相當於CSS的important語句,強制這個區域使用此ViewModel,不再往上查找同名屬性或方法!
另,爲了避免未經處理的原始模板內容在頁面載入時在頁面中一閃而過,我們可以使用以下樣式(詳見這裏):
.ms-controller,.ms-important{ |
忽略掃描綁定(ms-skip)
這是ms-skip負責。只要元素定義了這個屬性,無論它的值是什麼,它都不會掃描其他屬性及它的子孫節點了。
< div
ms-controller = "test"
ms-skip> |
A:< div
ms-each = "arr" >{{yy}}</ div > |
模板綁定(ms-include)
如果單是把DOM樹作爲一個模板遠遠不夠的,比如有幾個地方,需要重複利用一套HTML結構,這就要用到內部模板或外部模板了。
內部模板是,這個模板與目標節點是位於同一個DOM樹中。我們用一個MIME不明的script標籤或者noscript標籤(0.94後支持,建議使用它)保存它,然後通過ms-include="id"引用它。
< meta
http-equiv = "content-type"
content = "text/html; charset=UTF-8"
/> |
< script
src = "avalon.js" ></ script > |
< script
type = "avalon"
id = "tpl" > |
< div
ms-controller = "test" > |
< div
ms-include = "'tpl'" ></ div > |
注意,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) { |
text = loaded.apply(elem, [text].concat(vmodels)) |
avalon.innerHTML(elem, text) |
rendered && checkScan(elem, function() { |
外部模板,通常用於多個頁面的複用,因此需要整成一個獨立的文件。這時我們就需要通過ms-include-src="src"進行加載。
比如有一個HTML文件tmpl.html,它的內容爲:
< div >它是通過AJAX的GET請求加載下來的</ div > |
然後我們這樣引入它
< div
ms-include-src = "'tmpl.html'" ></ div > |
有關它的高級應用的例子可見這裏利用ms-include與監控數組實現一個樹
注意,ms-include-src需要後端服務器支持,因爲用到同域的AJAX請求。
數據填充(ms-text, ms-html)
這分兩種:文本綁定與HTML綁定,每種都有兩個實現方式
< 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 > |
默認情況下,我們是使用{{ }} 進行插值,如果有特殊需求,我們還可以配置它們:
注意,大家不要用<, > 作爲插值表達式的界定符,因爲在IE6-9裏可能轉換爲註釋節點,詳見這裏
插值表達式{{}}在綁定屬性的使用,只限那些能返回字符串的綁定屬性,如ms-attr、ms-css、ms-include、ms-class、 ms-href、 ms-title、ms-src等。一旦出現插值表達式,說明這個整個東西分成可變的部分與不可變的部分,{{}}內爲可變的,反之亦然。 如果沒有{{}}說明整個東西都要求值,又如ms-include="'id'",要用兩種引號強制讓它的內部不是一個變量。
類名切換(ms-class, ms-hover, ms-active)
avalon提供了多種方式來綁定類名,有ms-class, ms-hover, ms-active, 具體可看這裏
事件綁定(ms-on)
avalon通過ms-on-click或ms-click進行事件綁定,並在IE對事件對象進行修復,具體可看這裏
avalon並沒有像jQuery設計一個近九百行的事件系統,連事件回調的執行順序都進行修復(IE6-8,attachEvent添加的回調在執行時並沒有按先入先出的順序執行),只是很薄的一層封裝,因此性能很強。
- ms-click
- ms-dblclick
- ms-mouseout
- ms-mouseover
- ms-mousemove
- ms-mouseenter
- ms-mouseleave
- ms-mouseup
- ms-mousedown
- ms-keypress
- ms-keyup
- ms-keydown
- ms-focus
- ms-blur
- ms-change
- ms-scroll
- ms-animation
- ms-on-*
< script
src = "avalon.js"
type = "text/javascript" ></ script > |
avalon.ready(function() { |
array: ["aaa", "bbb", "ccc"], |
argsClick: function(e, a, b) { |
< fieldset
ms-controller = "simple" > |
< div
ms-click = "argsClick($event, 100, firstName)" >點我</ div > |
< div
ms-each-el = "array"
> |
< p
ms-click = "loopClick(el)" >{{el}}</ p > |
另外,這裏有一些結合ms-data實現事件代理的技巧,建議事件綁定接口支持事件代理,最簡單就是table上可以綁定td的點擊事件
顯示綁定(ms-visible)
avalon通過ms-visible="bool"實現對某個元素顯示隱藏控制,它用是style.display="none"進行隱藏。
插入綁定(ms-if)
這個功能是抄自knockout的,ms-if="bool",同樣隱藏,但它是將元素移出DOM。這個功能直接影響到CSS :empty僞類的渲染結果,因此比較有用。
< meta
http-equiv = "Content-Type"
content = "text/html; charset=utf-8"
/> |
< script
t src = "avalon.js" ></ script > |
< body
ms-controller = "test" > |
< ul
ms-each-item = "array" > |
< li
ms-click = "$remove"
ms-if = "$index % 2 == 0" >{{ item }} --- {{$index}}</ li > |
< script
type = "text/javascript" > |
array: "a,b,c,d,e,f,g".split(",") |
這裏得介紹一下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沒有某個屬性的情況。比如下面的情況:
< meta
http-equiv = "Content-Type"
content = "text/html; charset=utf-8"
/> |
< script
src = "avalon.js" ></ script > |
< body
ms-controller = "Test" > |
< ul
ms-if = "array"
ms-each-item = "array" > |
< li
ms-click = "$remove"
ms-if = "$index % 2 == 0" >{{ item }} --- {{$index}}</ li > |
< script
type = "text/javascript" > |
avalon.define('Test', function(vm) { |
如果沒有ms-if做代碼防禦,肯定報一大堆錯。
接着是 ms-repeat綁定。出於某些原因,我們不想顯示數組中的某些元素,就需要讓ms-if拖延到它們之後才起作用,這時就要用到ms-if-loop。
< meta
http-equiv = "Content-Type"
content = "text/html; charset=utf-8"
/> |
< script
src = "avalon.js" ></ script > |
< body
ms-controller = "test" > |
< li
ms-repeat = "array"
ms-if-loop = "el" >{{ el }}</ li > |
< script
type = "text/javascript" > |
array: ["aaa", "bbb", null, "ccc"] |
之後就是其他綁定,但殿後的總是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方法。
雙工綁定(ms-duplex)
這功能抄自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的詳細用法,大家可以通過這個頁面進行學習。
<meta http-equiv= "Content-Type"
content= "text/html; charset=UTF-8" > |
<div ms-controller= "box" > |
<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> |
<script src= "avalon.js"
></script> |
arr : [ "1" ,
'2' , "3" ,
"4" ], |
vm.checkAllbool = vm.arr.length === vm.selected.length |
vm.selected.$watch( "length" ,
function (n) { |
vm.checkAllbool = n === model.arr.size() |
對於非radio, checkbox, select的控件,我們可以通過data-duplex-changed來指定一個回調,傳參爲元素的value值,this指向元素本身,要求必須有返回值。
< meta
http-equiv = "Content-Type"
content = "text/html; charset=utf-8"
/> |
< title >data-duplex-changed</ title > |
< script
src = "avalon.js" ></ script > |
< body
ms-controller = "duplex" > |
< input
ms-duplex = "username"
data-duplex-changed = "callback" > |
< script
type = "text/javascript" > |
callback : function(val){ |
return this.value = val.slice(0, 10)//不能超過10個字符串 |
樣式綁定(ms-css)
用法爲ms-css-name="value"
注意:屬性值不能加入CSS hack與important!
< meta
http-equiv = "Content-Type"
content = "text/html; charset=UTF-8" > |
< script
src = "../avalon.js" ></ script > |
bg: "#F3F"// 不能使用CSS hack,如 bg : "#F3F\9\0" |
< body
ms-controller = "test"
> |
< h3 >在舊式IE下,如果父元素是定位元素,但沒有設置它的top, left, z-index,那麼爲它設置透明時, |
< div
class = "outer"
ms-css-opacity = "o"
ms-css-background-color = "bg"
> |
< div
class = "inner" ></ div > |
數據綁定(ms-data)
用法爲ms-data-name="value", 用於爲元素節點綁定HTML5 data-*屬性。
布爾屬性綁定1.3.5後,它們都吞入ms-attr-*
這主要涉及到表單元素幾個非常重要的布爾屬性,即disabed, readyOnly, selected , checked, 分別使用ms-disabled, ms-enabled, ms-readonly, ms-checked, ms-selected。ms-disabled與ms-enabled是對立的,一個true爲添加屬性,另一個true爲移除屬性。
字符串屬性綁定1.3.5後,除了ms-src, ms-href,其他都吞入ms-attr-*
這主要涉及到幾個非常常用的字符串屬性,即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)
ms-attr-name="value",這個允許我們在元素上綁定更多種類的屬性,如className, tabIndex, name, colSpan什麼的。
循環綁定(ms-repeat)
用法爲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。
- el: 不一定叫這個名字,比如說ms-each-item,它就變成item了。默認爲el。指向當前元素。
- $first: 判定是否爲監控數組的第一個元素
- $last: 判定是否爲監控數組的最後一個元素
- $index: 得到當前元素的索引值
- $outer: 得到外圍循環的那個元素。
- $remove:這是一個方法,用於移除此元素
我們還可以通過data-repeat-rendered, data-each-rendered來指定這些元素都插入DOM被渲染了後執行的回調,this指向元素節點, 有一個參數表示爲當前的操作,是add, del, move, index還是clear
vm.rendered = function(action){ |
avalon.log("渲染完畢")//注意,我們通過vm.array.push(4,5)添加元素,會連續兩次觸發rendered,第一次add,第二次爲index |
< li
data-repeat-rendered = "rendered"
ms-repeat = "array" >{{el}}</ li > |
< meta
http-equiv = "Content-Type"
content = "text/html; charset=UTF-8" > |
< script
src = "avalon.js" ></ script > |
array: ["1", "2", "3", "4"] |
array: [{name: "xxx", sex: "aaa", c: {number: 2}}, {name: "yyy", sex: "bbb", c: {number: 4}}]// |
a.array.push(5, 6, 7, 8, 9) |
a.array.unshift("a", "b", "c", "d") |
a.array.splice(1, 3, "x", "y", "z") |
< fieldset
class = "id2013716"
ms-controller = "array" > |
< li
>數組的第{{$index+1}}個元素爲{{el}}</ li > |
< p >size: < b
style = "color:red" >{{array.size()}}</ b ></ p > |
< fieldset
class = "id2013716"
ms-controller = "complex" > |
< li
ms-repeat-el = "array" >{{el.name+" "+el.sex}}它的內容爲 number:{{el.c.number}}</ li > |
< body
ms-controller = "page" > |
{{$index}} < button
ms-click = "$remove" >{{el}} 點我刪除</ button > |
< table
border = "1"
width = "800px"
style = "background:blueviolet" > |
{{el}} {{$first}} {{$last}} |
< li
ms-repeat = "arr" >< button
ms-click = "$remove" >測試{{$index}}</ button >{{el}}</ li > |
< li
ms-repeat = "object" >{{$key}}:{{$val}}</ li > |
< li >{{$key}}:{{$val}}</ li > |
< h3 >通過指定data-with-sorted規定只輸出某一部分建值及它們的順序,只能循環對象時有效</ h3 > |
< ol
ms-with = "bigobject"
data-with-sorted = "order"
title = 'with' > |
< li >{{$key}}:{{$val}}</ li > |
< li
ms-repeat = "bigobject"
data-with-sorted = "order" >{{$key}}:{{$val}}</ li > |
< 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
border = "1"
style = "background:green"
width = "400px" > |
< tbody
ms-each = "dbarray" > |
< tr
ms-each-elem = "el.array" >< td >{{elem}}</ td ></ tr > |
< 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 > |
var model = avalon.define({ |
arr : ["a", "b", "c", "d", "e", "f", "g", "h"] |
"kkk": "vvv", "kkk2": "vvv2", "kkk3": "vvv3" |
url: 'data/stockQuote.json', |
return ["name", "sortStatus", "sortName", "method", "align"] |
數組循環綁定(ms-each)
語法與ms-repeat幾乎一致,建議用ms-repeat代替。
對象循環綁定(ms-with)
語法爲 ms-with="obj" 子元素裏面用$key, $val分別引用鍵名,鍵值。另我們可以通過指定data-with-sorted回調,規定只輸出某一部分建值及它們的順序。 注意,此綁定已經不建議使用,它將被ms-repeat代替,ms-repeat裏面也可以使用data-with-sorted回調。
< meta
http-equiv = "Content-Type"
content = "text/html; charset=UTF-8" > |
< script
type = 'text/javascript'
src = "avalon.js" ></ script > |
< body
ms-controller = "xxx" > |
< div >{{$key}} {{$val}}</ div > |
< div >{{$key}} {{$val}}</ div > |
< div >{{$key}} {{$val}}</ div > |
有關ms-each, ms-repeat, ms-with更高的用法,如雙重循環什麼的,可以看這裏
UI綁定(ms-widget)
它的格式爲ms-widget="uiName, id?, optsName?"
- uiName,必選,一定要全部字母小寫,表示組件的類型
- id 可選 這表示新生成的VM的$id,方便我們從avalon.vmodels[id]中獲取它操作它,如果它等於$,那麼表示它是隨機生成,與不寫這個效果一樣,框架會在uiName加上時間截,生成隨機ID
- optName 可選, 配置對象的名字。指在已有的VM中定義一個對象(最好指定它爲不可監控的外),作爲配置的一部分(因爲每個UI都有它的默認配置對象,並且我們也可以用data- uiName? -xxx來做更個性化的處理 )。如果不指optName默認與uiName同名。框架總是找離它(定義ms-widget的那個元素節點)最近的那個VM來取這個配置項。如果這個配置項裏面有widget+"Id"這個屬性,那麼新生成的VM就是用它作爲它的$id
下面是一個完整的實例用於教導你如何定義使用一個UI。
模塊間通信及屬性監控 $watch,$fire, $unwatch
avalon內置了一個強大的自定義事件系統,它在綁定在每一個VM上。每一個VM都擁有$watch, $unwatch, $fire這三個方法,及一個$events對象。$events是用於儲存各種回調。先從單個VM說起,如果一個VM擁有aaa這個屬性,如果我們在VM通過$watch對它監控,那麼當aaa改變值時,它對應的回調就會被觸發!
var vmodel = avalon.define({ |
vmodel.$watch( "aaa" ,
function (newValue, oldValue){ |
注意,它只能監聽當前屬性的變動。
我們還可以通過$unwatch方法,移除對應的回調。如果傳入兩個參數,第一個是屬性名,第二個是回調,那麼只移除此回調;如果只傳入一個屬性名,那麼此屬性關聯的所有回調都會被移除掉。
有時,我們還綁定了一些與屬性名無關的事件回調,想觸發它,那隻能使用$fire方法了。$fire方法第一個參數爲屬性名(自定義事件名),其他參數隨意。
var vmodel = avalon.define({ |
vmodel.$watch( "cat" ,
function (){ |
avalon.log(avalon.slice(arguments))
|
vmodel.$fire( "cat" ,1,2,3) |
更高級的玩法,有時我們想在任何屬性變化時都觸發某一個回調,這時我們就需要$watch一個特殊的屬性了——“$all”。不同的是,$watch回調的參數多了一個屬性名,排在最前面。
var vmodel = avalon.define({ |
vmodel.$watch( "$all" ,
function (){ |
avalon.log(avalon.slice(arguments))
|
手動觸發$fire是位隨着高風險的,框架內部是做了處理(只有前後值發生變化纔會觸發),因此萬不得已使用它,但又爆發死循環怎麼辦?這樣就需要暫時中斷VM的屬性監控機制。使用$unwatch(),它裏面什麼也不傳,就暫時將監控凍結了。恢復它也很簡單,使用$watch(),裏面也什麼也不傳!
不過最強大的用法是實現模塊間的通信(因爲在實際項目中,一個頁面可能非常大,有多少人分塊製作,每個人自己寫自己的VM,這時就需要通過某種機制來進行數據與方法的聯動了),這是使用$fire方法達成的。只要在$fire的自定義事件名前添加"up!", "down!", "all!"前綴,它就能實現angular相似的$emit,$broadcast功能。
< meta
http-equiv = "Content-Type"
content = "text/html; charset=UTF-8" > |
< script
src = "avalon.js" ></ script > |
var vm1 = avalon.define({ |
vm1.$fire("down!aaa", "capture") |
vm1.$watch("aaa", function(v) { |
avalon.log("ancestor.aaa事件被觸發了") |
var vm2 = avalon.define({ |
vm2.$fire("all!aaa", "broadcast") |
vm2.$watch("aaa", function(v) { |
avalon.log("parent.aaa事件被觸發了") |
vm3.$fire("up!aaa", "bubble") |
vm3.$watch("aaa", function(v) { |
avalon.log("son.aaa事件被觸發了") |
< body
class = "ms-controller"
ms-controller = "ancestor" > |
< h3 >avalon vm.$fire的升級版 </ h3 > |
< button
type = "button"
ms-click = "click" > |
< div
ms-controller = "parent" > |
< button
type = "button"
ms-click = "click" >broadcast</ button > |
< div
ms-controller = "son" > |
< button
type = "button"
ms-click = "click" > |
過濾器
avalon從angular中抄來管道符風格的過濾器,但有點不一樣。 它只能用於{{}}插值表達式。如果不存在參數,要求直接跟|filter,如果存在參傳,則要用小括號括起,參數要有逗號,這與一般的函數調用差不多,如|truncate(20,"……")
avalon自帶以下幾個過濾器
- html
- 沒有傳參,用於將文本綁定轉換爲HTML綁定
- sanitize
- 去掉onclick, javascript:alert等可能引起注入攻擊的代碼。
- uppercase
- 大寫化
- lowercase
- 小寫化
- truncate
- 對長字符串進行截短,truncate(number, truncation), number默認爲30,truncation爲“...”
- camelize
- 駝峯化處理
- escape
- 對類似於HTML格式的字符串進行轉義,把尖括號轉換爲> <
- currency
- 對數字添加貨幣符號,以及千位符, currency(symbol)
- number
- 對數字進行各種格式化,這與與PHP的number_format完全兼容, number(decimals, dec_point, thousands_sep),
decimals 可選,規定多少個小數位。
dec_point 可選,規定用作小數點的字符串(默認爲 . )。
thousands_sep 可選,規定用作千位分隔符的字符串(默認爲 , ),如果設置了該參數,那麼所有其他參數都是必需的。
- date
- 對日期進行格式化,date(formats)
'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) |
'EEEE': Day in Week,(Sunday-Saturday) |
'EEE': Day in Week, (Sun-Sat) |
'HH': Hour in day, padded (00-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) |
'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){ |
AMD 加載器
avalon裝備了AMD模範的加載器,這涉及到兩個全局方法 require與define
require(deps, callback)
deps 必需。String|Array。依賴列表,可以是具體路徑或模塊標識,如果想用字符串表示多個模塊,則請用“,”隔開它們。
callback 必需。Function。回調,當用戶指定的依賴以及這些依賴的依賴樹都加載執行完畢後,纔會安全執行它。
如果想禁止使用avalon自帶的加載器,可以在第一次調用require方法之前,執行如下代碼:
<meta http-equiv= "Content-Type"
content= "text/html; charset=UTF-8" > |
<script src= "require.js" ></script> |
<script src= "avalon.modern.js" ></script> |
avalon.config({loader:
false }) |
avalon.define( "xxx" ,
function (vm){ |
<body ms-controller= "xxx"
> |
與jquery更好的集成,比如一些舊系統,直接在頁面引入jquery庫與其大量jquery插件,改成動態加載方式成本非常大。怎麼樣才能與jquery和平共存,亦能讓AMD加載發揮作呢?先引入jquery庫, 然後將avalon.modules.jquery 加個預設值(exports: jquery用於shim機制, state: 2 表明它已經加載完畢)就行了。
如果你想用其他AMD加載器,最好的辦法還是建議直接打開源碼,拉到最底幾行,把加載器禁用了!
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
AJAX可以使用jQuery或mmRequest, mmRequest體積更少,覆蓋jQuery ajax模塊的90%功能,並且在現代瀏覽器中使用了XMLHttpRequest2實現,性能更佳。
通過AJAX加載新數據到已存在的VM中
data: JSON.parse(JSON.stringify(vm.$model)),
|
success:
function (ajaxData) { |
ajaxData = filterData(ajaxData) |
var
newData = avalon.mix( true , {}, vm.$model, ajaxData) |
if
(vm.hasOwnProperty(i) && i !== "hasOwnProperty" ){ |
提交VM中的數據到後臺,要小心死循環,詳見這裏
文件上傳要用mmRequest的upload方法
擴展功能
avalon現在有三個擴展點,一是在avalon.fn上添加新的原型方法,這是用於處理DOM的,二是在avalon.bindingHandlers與 avalon.bindingExecutors上添加新的綁定(ms-xxx),三是在avalon.filters添加新的過濾器。
添加原型方法就不用多說,建議儘可能返回this,實現鏈式操作,this[0]爲它包含的元素節點。
添加過濾器也很簡,翻看源碼看看lowercase如何實現就行了。
添加新綁定難一點,框架bindingHandlers要求對應的處理函數有兩個參數,data與vmodels, data擁有如下幾個屬性:
- element: 綁定了ms-xxx的元素,如<div ms-xxx-yyy='zzz'>innerHTML</div>,ms-xxx綁定所在的DIV元素。
- value:是指mx-xxx綁定的這個特性節點的值,即上面的zzz。
- param:是指mx-xxx綁定名以“-”分開幾截,除了最前面的兩部分外的東西,如這裏的“yyy”。
vmodels是指,從DOM樹最頂點到添加此綁定的元素所路過的ms-controller的值(它們都對應一個VM)。注意,ms-each, ms-with也產生VM。
bindingHandlers裏的函數用於初始化綁定,它會對綁定屬性做一些分解,放進parseExprProxy中,parseExprProxy會再調用parseExpr,將它轉換爲求值函數,放進行對應VM屬性的subscribers數組內(操作方爲registerSubscriber)。
bindingExecutors裏的的函數爲真正的視圖刷新函數,每當VM發生改變後,都會被執行(操作方爲notifySubscribers)。
可看這裏
現在avalon擁有如此多綁定:
在IE6下調試avalon
由於IE6下沒有console.log,如果又不想用VS等巨無霸IDE,可以自己定義以下方法
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回調