1. 程式人生 > >迷你MVVM框架 avalonjs 入門教程

迷你MVVM框架 avalonjs 入門教程

新官網

請不要無視這裡,這裡都是連結,可以點的

學習教程 視訊教程: 地址1 地址2 錨點路由 8.1. 路由定義 8.2. 引數定義 8.3. 業務處理 定義模板變數標識標籤 AJAX 10.1. HTTP請求 10.2. 廣義回撥管理 工具函式 11.1. 上下文繫結 11.2. 物件處理 11.3. 型別判定 其它服務 12.1. 日誌 12.2. 快取 12.3. 計時器 12.4. 表示式函式化 12.5. 模板單獨使用 自定義模組和服務 13.1. 模組和服務的概念與關係 13.2. 定義模組 13.3. 定義服務 13.4. 引入模組並使用服務 附加模組 ngResource 14.1. 使用引入與整體概念 14.2. 基本定義 14.3. 基本使用 14.4. 定義和使用時的佔位量 http://zouyesheng.com/angular.html

關於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 :

 <!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程式碼專注業務邏輯本身, 寫得更易維護的程式碼!

掃描

不過上面的程式碼並不完整,它能工作,是因為框架預設會在DOMReady時掃描DOM樹,將檢視中的繫結屬性與{{}}插值表示式抽取出來,轉換為求值函式與檢視重新整理函式。

上面的JS程式碼相當於:

            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
                 }
            })
        

接著我們說一些重要的概念:

  • $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定義之後,再追加新屬性與方法,比如下面的方式是錯誤的:

                 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的繫結(或指令),擁有以下三種類型:

  • {{}}插值表示式, 這是開標籤與閉標籤間,換言之,也是位於文字節點中,innerText裡。{{}}裡面可以新增各種過濾器(以|進行標識)。值得注意的是{{}}實際是文字繫結(ms-text)的一種形式。
  • ms-*繫結屬性, 這是位於開標籤的內部, 95%的繫結都以這種形式存在。 它們的格式大概是這樣劃分的"ms" + type + "-" + param1 + "-" + param1 + "-" + param2 + ... + number = value
                        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"//這個繫結屬性有三個引數,表示三種不同的攔截操作 
                    
  • 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>
        <div ms-important="DDD">
            <div>{{name}} :  {{color}}</div>
        </div>
    </div>
</div>
            

ViewModel

   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()
  })
            
{{name}} : {{color}} {{name}} : {{color}} {{name}} : {{color}} {{name}} : {{color}}

可以看出ViewModel在DOM樹的作用範圍其實與CSS很相似,採取就近原則,如果當前ViewModel沒有此欄位 就找上一級ViewModel的同名欄位,這個機制非常有利於團隊協作。

如果從另一個角度來看,由於這種隨機組成的方式就能實現類似繼承的方式,因此我們就不必在JS程式碼時構建複雜的繼承體系

類的繼承體系是源自後端複雜業務的膨脹而誕生的。早在20世界80年代初期,也就是面向物件發展的初期,人們就非常看重繼承這個概念。 繼承關係蘊涵的意義是非常深遠的。使用繼承我們可以基於差異程式設計,也就是說,對於一個滿足我們大部分需求的類,可以建立一個它的子類,過載它個別方法來實現我們所要的功能。只子繼承一個類, 就可以重類該類的程式碼!通過繼承,我們可以建立完整的軟體結構分類,其中每一個層都可以重用該層次以上的程式碼。這是一個美麗新世界。

但類繼承的缺點也是很明顯的,在下摘錄一些:

面嚮物件語言與生俱來的問題就是它們與生俱來的這一整個隱性環境。你想要一根香蕉,但你得到的是一頭手裡握著香蕉的大猩猩,以及整個叢林。 -- Joe Armstrong
在適合使用複合模式的共有類中使用繼承,會把這個類與它的超類永遠地束縛在一起,從而人為地限制了子類的效能

類繼承的缺點

  1. 超類改變,子類要跟著改變,違反了“開——閉”原則
  2. 不能動態改變方法實現,不能在執行時改變由父類繼承來的實現
  3. 破壞原有封裝,因為基類向子類暴露了實現細節
  4. 繼承會導致類的爆炸

因此在選擇是繼承還是組合的問題上,avalon傾向組合。組合的使用範例就是CSS,因此也有了ms-important的誕生。

而ms-important就相當於CSS的important語句,強制這個區域使用此ViewModel,不再往上查詢同名屬性或方法!

另,為了避免未經處理的原始模板內容在頁面載入時在頁面中一閃而過,我們可以使用以下樣式(詳見這裡):

   .ms-controller,.ms-important,[ms-controller],[ms-important]{
        visibility: hidden;
    }
        

忽略掃描繫結(ms-skip)

這是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>
        

模板繫結(ms-include)

如果單是把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-src需要後端伺服器支援,因為用到同域的AJAX請求。

資料填充(ms-text, ms-html)

這分兩種:文字繫結與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'",要用兩種引號強制讓它的內部不是一個變數。

類名切換(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-*
<!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-visible)

avalon通過ms-visible="bool"實現對某個元素顯示隱藏控制,它用是style.display="none"進行隱藏。


插入繫結(ms-if)

這個功能是抄自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方法。


雙工繫結(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的詳細用法,大家可以通過這個頁面進行學習。

<!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)

用法為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)

用法為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。

  1. el: 不一定叫這個名字,比如說ms-each-item,它就變成item了。預設為el。指向當前元素。
  2. $first: 判定是否為監控陣列的第一個元素
  3. $last: 判定是否為監控陣列的最後一個元素
  4. $index: 得到當前元素的索引值
  5. $outer: 得到外圍迴圈的那個元素。
  6. $remove:這是一個方法,用於移除此元素

我們還可以通過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-each)

語法與ms-repeat幾乎一致,建議用ms-repeat代替。

物件迴圈繫結(ms-with)

語法為 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更高的用法,如雙重迴圈什麼的,可以看這裡

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。

例子

首先,以AMD規範定義一個模組,檔名為avalon.testui.js,把它放到與avalon.js同一目錄下。內容為:

define(["avalon"], function(avalon) {
    //    必須 在avalon.ui上註冊一個函式,它有三個引數,分別為容器元素,data, vmodels
    avalon.ui["testui"] = function(element, data, vmodels) {
      //將它內部作為模板,或者使用文件碎片進行處理,那麼你就需要用appendChild方法添加回去
        var innerHTML = element.innerHTML
        //由於innerHTML要依賴許多widget後來新增的新屬性,這時如果被掃描肯定報“不存在”錯誤
        //因此先將它清空
        avalon.clearHTML(element)
        var model = avalon.define(data.testuiId, function(vm) {
            avalon.mix(vm, data.testuiOptions)//優先新增使用者的配置,防止它覆蓋掉widget的一些方法與屬性
            vm.value = 0; // 給input一個個預設的數值
            vm.plus = function(e) { // 只添加了這個plus
                model.value++;
            }
        })
        avalon.nextTick(function() {
            //widget的VM已經生成,可以添加回去讓它被掃描
            element.innerHTML = innerHTML
            avalon.scan(element, [model].concat(vmodels))
        })
        return model//必須返回新VM
    }
    avalon.ui["testui"].defaults = {
        aaa: "aaa",
        bbb: "bbb",
        ccc: "ccc"
    }
    return avalon//必須返回avalon
})
     
        

然後頁面這樣使用它

        
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <script src="avalon.js"></script>
    </head>
    <body>
        <script>
            require(["avalon.testui"], func