迷你MVVM框架 avalonjs 沉思錄 第3節 動態模板


模板的發明是編程史上的一大里程碑,讓我們擺脫了煩鎖且易出錯的字符串拼接,維護性大大提高。

都在JSP,ASP時代,人們已經學會使用include等語句,將多個頁面片斷拼接成一個頁面。 此外,為了將數據庫中的數據或業務中用到的變量輸出到頁面,我們需要將頁面某個地方標記一下,將變量塞到里面去。 最后,出於方便循環輸出一組數據,就需要將each語句從HTML里撕開一道口子,加上其他什么if語句,頁面上其實變撕裂成兩部分 一種是與后端語言相近的邏輯部分,一個是夠為純凈的HTML部分,到最后,模板引擎就發展出來。

在jQuery王朝的后期,業務邏輯不斷往前搬,前端模板也發明出來了。這些模板我統稱為靜態模板或字符串模板,特征是模板是放在 一個script標簽或textarea標簽里。靜態模板的好處是統一管理,我們從script標簽等抽取內容時,它是原汁原味,沒有被竄改。 缺點是破壞原有的結構。MVVM時代,knockout, ember等率先發明動態模板,或叫DOM模塊,特點是通過在元素節點上標記一些特殊屬性,注明此元素里面會輸出什么內容 或此元素的子元素是作用循環體要循環多少次,當然if等輸出不輸出很小兒科。缺點是,需要對文檔的整體或某一區域進行掃描,這里耗時比靜態模板多上幾倍,並且定界符(用於輸出變量的標記)可能離奇失蹤。但這也沒什么大不了,現在流行的兩種定界符形式≈lt&;, %>{{ }}在IE10+或W3C瀏覽器活得好好的,IE6-9,我們只要避開大於小於號就行了。 此外動態模板與靜態模板最大的不同在於,它是沒有編譯函數,而是通過掃描文檔,根據節點上的定界符與綁定屬性實現循環輸出,填空等功能。

我們看一下avalon是怎么做的。大致分兩塊,定義VM,添加綁定。VM是我們操作的主體,綁定是將頁面變成模板的關鍵。

VM的定義

avalon.define("test",function(vm){
   vm.aaa = "司徒正美"
})

avalon.define是用來定義VM,第一個參數為VM的ID名,這是用於在頁面圈定作用域的范圍,對應的綁定屬性是ms-controller。因為一個頁面可能有多人負責,就存在多個VM了,而VM相當於一個數據據,它們都用於不同的區域,這里就需要用ID來區分了。

添加綁定,我們隨便往body一塞就行了

<body>{{aaa}}</body>

這里的{{ }}是定界符,放在文本節點里。我們可以用過avalon.config({interpolate: ["{?", "?}"]})來設置定界符。

<!DOCTYPE html>
<html>
    <head>
        <title>avalon</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
       <script src="http://files.cnblogs.com/rubylouvre/avalon20130929.js">   </script>
        <script>
            avalon.define("test", function(vm){
                vm.aaa = "司徒正美"
            })
        </script>
    </head>
    <body ms-controller="test">
        <h3>{{aaa}}</h3>
        {{aaa}}
    </body>
</html>

但世界上沒有這么簡單的頁面,比如我們要輸出一個列表,是不是要這樣干呢?

<!DOCTYPE html>
<html>
    <head>
        <title>avalon</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
        <script src="http://files.cnblogs.com/rubylouvre/avalon20130929.js"></script>
        <script>
            avalon.define("test", function(vm) {
                vm.a = "2014預言:阿里死磕港交所"
                vm.b = "馬雲進軍游戲背后:恐失勢電商"
                vm.c = "支付寶信息泄露揭大公司管理困境"
                vm.d = "盤點2013:智能手機開啟的新場景"
            })
        </script>
    </head>
    <body ms-controller="test">
        <ol>
            <li>{{a}}</li>
            <li>{{b}}</li>
            <li>{{c}}</li>
            <li>{{d}}</li>
        </ol>
    </body>
</html>

當然不行,這要定義多少個變量啊!這時就需用到循環綁定,ms-repeat!

<!DOCTYPE html>
<html>
    <head>
        <title>avalon</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
        <script src="http://files.cnblogs.com/rubylouvre/avalon20130929.js"></script>
        <script>
            avalon.define("test", function(vm) {
                vm.array = ["2014預言:阿里死磕港交所", 
                    "馬雲進軍游戲背后:恐失勢電商",
                    "支付寶信息泄露揭大公司管理困境", "盤點2013:智能手機開啟的新場景"]
            })
        </script>
    </head>
    <body ms-controller="test">
        <ol>
            <li ms-repeat="array">{{el}}</li>
        </ol>
    </body>
</html>

ms-repeat相當於ms-each-el,后面的-el是可配置可省略。比如改成ms-repeat-elem,那么對應的位置要改成{{elem}}。

我們還可以輸出2維數組

<!DOCTYPE html>
<html>
    <head>
        <title>avalon</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
        <script src="http://files.cnblogs.com/rubylouvre/avalon20130929.js"></script>
        <script>
            avalon.define("test", function(vm) {
                vm.array = [[1, 2], [4, 5], [7, 8]]
            })
        </script>
    </head>
    <body ms-controller="test">
        <table width="80%" border="1">
            <tr ms-repeat-item="array">
                <td ms-repeat="item">{{el}}</td>
            </tr>
        </table>
    </body>
</html>

輸出對象數組

<!DOCTYPE html>
<html>
    <head>
        <title>avalon</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
        <script src="http://files.cnblogs.com/rubylouvre/avalon20130929.js"></script>
        <script>
           var model =  avalon.define("test", function(vm) {
                vm.array = [{name: 111}, {name: 222}, {name: 333}]
            })
        </script>

    </head>
    <body ms-controller="test">
        <ul>
            <li ms-repeat-me="array">{{me.name}}</li>
        </ul>
    </body>
</html>

現在大家算是對ms-repeat算是有一個大體的了解吧。那么我們學一點高級的。avalon.define會返回一個VM對象,我們通過操作它就能實現頁面的操作數據即操作DOM!!!,比如vmodel.array.push。另,我們想輸出每個元素對應的索引值,可以使用$index這個變量。

<!DOCTYPE html>
<html>
    <head>
        <title>avalon</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
        <script src="http://files.cnblogs.com/rubylouvre/avalon20130929.js"></script>
        <script>
           var model =  avalon.define("test", function(vm) {
                vm.array = [{name: 111}, {name: 222}, {name: 333}]
            })
            var array = model.array
            setInterval(function(){
                array.push({name: Math.random().toString(32).substr(4,14)})
                if(array.length > 10){
                    array.shift()
                }
            },500)
        </script>

    </head>
    <body ms-controller="test">
        <ul>
            <li ms-repeat="array">{{$index}}--{{el.name}}</li>
        </ul>
    </body>
</html>


之所以 能有這樣神奇的效果,是因為avalon會將VM中的數組轉換為監控數組,它擁有以下方法:

push, shift, unshift, pop, slice, splice, remove, removeAt, removeAll, clear, ensure, sort, reverse, set

現在我們跳前一步,學一下ms-on-*綁定,實現一個更復雜的效果。ms-on-*的*對應一個事件名,屬性值為VM中一個函數名,與元素onkeypress, onclick一樣,它的第一個參數默認是事件對象,this指向元素節點,不同的是我們已經對IE6-8下的事件對象做了兼容處理。

<!DOCTYPE HTML>
<html>
    <head>
        <title>ms-repeat</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 
        <script src="http://files.cnblogs.com/rubylouvre/avalon20130929.js"></script>
        <script>
            avalon.define("test", function(vm) {
                vm.array = ["1", "2", "3", "4"]
                "push,unshift,remove,ensure".replace(/\w+/g, function(method) {
                    vm[method] = function(e) {
                        if (this.value && e.which == 13) {//this為input元素
                            vm.array[method](this.value);
                            this.value = "";
                        }
                    }
                })

                vm.removeAt = function(e) {
                    if (isFinite(this.value) && e.which == 13) {//this為input元素
                        var a = ~~this.value
                        vm.array.removeAt(a)
                        this.value = "";
                    }
                }
                "pop,shift,sort,reverse".replace(/\w+/g, function(method) {
                    vm[method] = function(e) {
                        vm.array[method]();
                    }
                })
            });



        </script>
    </head>
    <body ms-controller="test">
        <p>監控數組擁有以下方法,我們可以操作它們就能同步對應的區域</p>
        <blockquote>
            push, shift, unshift, pop, slice, splice, remove, removeAt, removeAll, clear, ensure, sort, reverse, set
        </blockquote>
        <ul>
            <li  ms-repeat="array">數組的第{{$index+1}}個元素為{{el}}</li>
        </ul>
        <p>對數組進行push操作,並回車<input ms-on-keypress="push"></p>
        <p>對數組進行unshift操作,並回車<input ms-on-keypress="unshift"></p>
        <p>對數組進行ensure操作,並回車<input ms-on-keypress="ensure"><br/>
            (只有數組不存在此元素才push進去)</p>
        <p>對數組進行remove操作,並回車<input ms-on-keypress="remove"></p>
        <p>對數組進行removeAt操作,並回車<input ms-on-keypress="removeAt"></p>
        <p><button type="button" ms-on-click="sort">對數組進行sort操作</button></p>
        <p><button type="button" ms-on-click="reverse">對數組進行reverse操作</button></p>
        <p><button type="button" ms-on-click="shift">對數組進行shift操作</button></p>
        <p><button type="button" ms-on-click="pop">對數組進行pop操作</button></p>
        <p>當前數組的長度為<span style="color:red">{{array.size()}}</span>,注意 我們無法修改數組length的固有行為,因此它無法同步視圖,需要用size方法。</p>
    </body>
</html>

有了批量輸出的ms-repeat及通過調用監控數組的方法就能實現對應節點的刪除添加排序,那么實現grid簡直易如反掌。

<!DOCTYPE HTML>
<html>
    <head>
        <title>ms-repeat</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 
        <script src="http://files.cnblogs.com/rubylouvre/avalon20130929.js"></script>
        <script>
            if (!Date.now) {
                Date.now = function() {
                    return new Date - 0;
                }
            }
            avalon.define('test', function(scope) {
                scope.selected = "name"
                scope.options = ["name", "size", "date"]
                scope.trend = 1
                scope.data = [
                    {name: "aaa", size: 213, date: Date.now() + 20},
                    {name: "bbb", size: 4576, date: new Date - 4},
                    {name: "ccc", size: 563, date: new Date - 7},
                    {name: "eee", size: 3713, date: 9 + Date.now()},
                    {name: "555", size: 389, date: Date.now() - 20}
                ];
                scope.$watch("selected", function(v) {
                    var t = parseFloat(scope.trend)
                    scope.data.sort(function(a, b) {
                        var ret = a[v] > b[v] ? 1 : -1
                        return t * ret
                    })
                })
                scope.$watch("trend", function(t) {
                    var v = scope.selected, t = parseFloat(t)
                    scope.data.sort(function(a, b) {
                        var ret = a[v] > b[v] ? 1 : -1
                        return t * ret
                    })
                })
            });


        </script>
    </head>
    <body ms-controller="test">
        <p>
            <select ms-duplex="selected">
                <option ms-repeat="options">{{el}}</option>
            </select>
            <select ms-duplex="trend">
                <option value="1">up</option>
                <option value="-1">down</option>
            </select>
        </p>
        <table width="500px" border="1">
            <tbody >
                <tr ms-repeat="data">
                    <td>{{el.name}}</td> <td>{{el.size}}</td> <td>{{el.date}}</td>
                </tr>
            </tbody>
        </table>
    </body>
</html>

這里用到了ms-duplex, $watch,大家可以到《入門教程》看看,都是很簡單的東西。

接着我們再看看如何循環輸出對象吧,它也是用ms-repeat,不過里面的變量為$key, $val。不用多言,看例子。

<!DOCTYPE html>
<html>
    <head>
        <title>avalon</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
        <script src="http://files.cnblogs.com/rubylouvre/avalon20130929.js"></script>
        <script>
            var model = avalon.define("test", function(vm) {
                vm.object = {
                    grape: "葡萄",
                    coconut: "椰子",
                    pitaya: "火龍果",
                    orange: "橙子"
                }
                
            })

        </script>

    </head>
    <body ms-controller="test">
        <ul>
            <li ms-repeat="object">{{$key}}--{{$val}}</li>
        </ul>
    </body>
</html>

好了,循環輸出就到這里。我們最后看一下如何實現其他模板引擎的if語句。它的名字為ms-if,如果值為真就輸出,否則不輸出。

<!DOCTYPE html>
<html>
    <head>
        <title>avalon</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
        <script src="http://files.cnblogs.com/rubylouvre/avalon20130929.js"></script>
        <script>
            var model = avalon.define("test", function(vm) {
                vm.toggle = true
                vm.click = function(){
                    vm.toggle = !vm.toggle
                }
                vm.text = "捉迷藏"
                
            })

        </script>

    </head>
    <body ms-controller="test">
        <p ms-if="toggle">{{text}} </p>
        <button type="button" ms-on-click="click">點我{{toggle ?  '隱藏' : "顯示"}}</button>
    </body>
</html>

{{}}, ms-repeat, ms-if這就是動態模板相對靜態模板的所有功能了,但由於動態模板在掃描之后,得到所有要處理的節點的引用,這也意味着,以后我們要做一小部分的更新,不用像靜態模板那樣大規模替換,而是細化到每一個元素節點,特性節點或文本節點。這就是所謂的“最小化刷新”技術。一般的,只有ms-if等少量綁定才影響到元素節點那一層面,更多的時候, 我們是在刷新特性節點的value值,文本節點的data值,這也意味着,我們的刷新不會引起reflow。加之,能得到元素節點本上,我們就可以輕松實現綁定事件,操作樣式,修改屬性等功能。這也是為什么大多數MVVM框架選擇動態模板的緣故,jQuery原來可以做的,我們全部通過綁定屬性或定界符在HTML里搞定。 這也意味着,我們實現了完美的分層架構,JS里面是純粹的模型層(包括model與viewmodel),HTML里是學習成本與維護成本極低的視圖層。這已經不是多了一個模板引擎這么簡單的事,我們搶到了 一直以來屬性於后端的禁臠——分層架構


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM