zoukankan      html  css  js  c++  java
  • 迷你MVVM框架 avalonjs1.5 入门教程

    avalon经过几年以后,已成为国内一个举足轻重的框架。它提供了多种不同的版本,满足不同人群的需要。比如avalon.js支持IE6等老旧浏览器,让许多靠政府项目或对兼容性要求够高的公司也能享受MVVM的乐趣。avalon.modern.js支持IE10以上版本,优先使用新API,性能更优,体积更少。avalon.mobile.js在avalon.modern的基础提供了触屏事件的支持,满足大家在移动开发的需求。此外,它们分别存在avalon.xxx.shim版本,指无自带加载器版,avalon.xxx.min版本,指上线压缩版本。

    avalon早期严重受到angular与knockout的影响,API与它们很相近,经过多年的发展,渐渐摸索出自己一套模式。腾讯百度阿里等大公司都有部门在使用avalonjs,这有力促进avalon的发展。avalon1.5是一个里程碑的版本,它带来许多全新的特性,让我们编写代码更加爽快。

    avalon1.5的下载地址: https://github.com/RubyLouvre/avalon/tree/1.5

    1. 视图模型
    2. 非监控属性与监控属性
    3. 视图模型的作用域
    4. 扫描机制
    5. 指令(绑定)
    6. 数据填充(ms-text, ms-html)
    7. 模板绑定(ms-include)
    8. 类名切换(ms-class, ms-hover, ms-active)
    9. 事件绑定(ms-on,……)
    10. 显示绑定(ms-visible)
    11. 插入绑定(ms-if)
    12. 双工绑定(ms-duplex)
    13. 样式绑定(ms-css)
    14. 数据绑定(ms-data)
    15. 属性绑定(ms-attr)
    16. 循环绑定(ms-repeat,ms-each,ms-with)
    17. 动画绑定(ms-effect)
    18. 自定义标签组件
    19. 模块间通信及属性监控 $watch,$fire
    20. 过滤器
    21. 自定义指令(绑定)
    22. 加载器
    23. AJAX
    24. 路由系统
    25. 在IE6下调试avalon
    26. avalon1.4.6到1.5的升级指南
    27. 其他要注意的地方(更新VM等)

    视图模型

    avalon与jQuery最大的一个区别是,思维的不同。jQuery要操作一个元素,总是设法找到此元素,想象这个元素是否有ID,有某个类名,存在某个特定的标签下,是父节点的第几个孩子,诸如此类,最后拼凑出一个CSS表达式,然后$(expr)找到元素,然后再进行操作,于是JS代码里满屏$。维护代码的人,总是要对着页面来看看,这表达式是对应某某元素,如果只有ID,类名还好,新手很是写出很长的CSS表达式,导致你最后崩溃掉。

    avalon要操作某个元素,就直接在HTML为它添加一些指令,这些指令或者以ms-开头的元素属性,或是标签之间的4个花括号。指令里面存在某些变量,这些变量最后在JS聚集成一个对象,这就叫做VM(View Model, 视图模型)。我们只要操作这个VM的数据变动就行了,页面上就会自动变化。有了这一层的分离,我们在代码量就少能许多操作DOM的代码,专致于业务本身。比如说:

    <p>{{aaa}}</p>
    

    相当于jQuery的以下代码:

    $(function(){
       $("p").text(aaa)
    })
    

    那我们看看怎么定义一个VM吧。avalon在1.5之前存在两种定义方式,现在1.5只支持新风格,即

    var vm = avalon.define({
        $id: "test",
        a: 1,
        b: 2,
        c: {
          d: 1
        },
        onClick: function(e){
           e.preventDefault()
        },
        arr: [1,2,3]
    })
    

    avalon.define是一个非常重要的方法,要求传入一个对象,对象里面必须有$id属性,它是用于指定其在页面的作用范围。

    avalon.define会返回一个新对象,它除了之前我们定的属性与方法,还添加了$watch, $events, $fire, $model等属性与方法。

    当我们以vm.a = 4来重新赋值时,页面上用到a的地方会自然作出反应,这个行为称之为绑定,有的属性会使用ms-duplex指令绑定到表单元素上,这时反应是双向的,input,select, textarea的值被用户改动时,会自然反应到VM上,而我们对VM上的操作也会反应到表单元素上,这叫做双工绑定

    有的东西,你压底只有它只作用一次,如大表格的数据展示,以后没有任何互动交互,那我们有几种方式:

    • 将该属性的名字以$开头,以$aaa,这样标识它为非监控属性。
    • 将该属性的名字放到$skipArray数组,也能标识它非监控属性,因为有的名字需要前后一致,后端不愿意加$,这种方式更灵活。
    • 有的属性我想它在这里可以多次改动,有的则显示就不改了,可以使用单向绑定,需要在ms-*属性的值前面加::,或花括号内部的前面加::
    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <script src="avalon.js"></script>
            <script>
                var vm = avalon.define({
                    $id: "test",
                    a: 1,
                    $b: 2,
                    $skipArray: ["a"],
                    c: 3
                })
                setTimeout(function () {
                    vm.a = 100
                    vm.$b = 100
                    vm.c = 100
                }, 3000)
            </script>
        </head>
        <body>
            <div ms-controller="test">
                <p>{{a}}不会变</p>
                <p>{{$b}}不会变</p>
                <p>{{::c}}不会变</p>
                <p>{{c}}会变</p>
            </div>
        </body>
    </html>
    

    非监控属性与监控属性

    VM是一个对象,它除了包含一些必须的方法与属性外,其他的东西就分为两大类,非监控属性与监控属性。

    非监控属性,就是我们上面指的以$开头,或是名字定义在$skipArray数组的东西,此外,当某属性的值的类型为函数或元素节点,文本节点,注释节点,文档对象,文档碎片与window时,它们也无法监控。

    监控属性则分为4类:

    • 当其值为字符串,数值,布尔,undefined, null等简单数据类型时,为监控属性
    • 当它定义在$computed对象中时,其值为一个对象,拥有get,set方法,为计算属性。计算属性总是有一个或一个以上的监控属性构成。
    • 当其值的类型为数组时,称之为监控数组,我们可以改动它的方法,来同步视图。在1.5中,我们不断可以监听其长度变化,还可以监听其元素或元素的属性变化。
    • 当其值为一个对象时,它里面的对象也继续转换为计算属性,监控属性,这个对象我们称之为子VM

    在1.5之前的版本,还有一个叫监控函数的东西,即里面包含了某些监控方法。但现在我们不建议这样用,因为在未来的版本,我们打算像angular那样通过纯静态词法分析,就能得到此指令所依赖的所有监控属性。而监控方法则需要使用动态的依赖检测实现。动态依赖检测虽然非常强大,但也非常耗性能。在1.5之前,avalon是完全通过动态依赖检测实现绑定的,1.5是结合静态词法分析与动态依赖检测,未来会一点点改为纯静态词法分析。

                var vm = avalon.define({
                    $id: "test",
                    a: 1,
                    $b: 2,
                    $skipArray: ["a"],
                    c: 3, //监控属性
                    d: {  //这是子VM
                        dd: {
                            ddd: 3
                        },
                        dd2: 4
                    },
                    arr: [1, 2, 3, 4], //监控数组
                    $computed: {
                        c: {//计算属性c
                            get: function () {
                                return this.a + " " + this.c
                            },
                            set: function (val) {
                                var arr = val.split(" ")
                                this.a = arr[0]
                                this.b = arr[1]
                            }
                        },
                        e: {//计算属性e
                            get: function () {
                                return this.a + 100
                            }
                        }
                    }
                })
    

    视图模型的作用域

    为了方便协作开发的需求,我们引入了作用域的概念。因为一个页面可能很大,分为N个模块,每个模块交同不同的人来编写。这个在移动端的SPA应用中尤为明显。 对于JS,我们可以拆分为N个JS文件,每个JS文件都有自己的VM。页面也是拆成一块块,这可以通过PHP或nodejs的模板贴合起来。而在这之前,我们先为它们加上ms-controller!

    ms-controller为一个指令,其值为一个VM的$id,如ms-controller="test",它就会在avalon.vmodels中找到该VM,然后这个元素下方用到的所有指令中的变量,都应该位于此VM。

    但如果一个功能模块特别复杂,它用到的字段特别多,意味着这个VM也要定义许多许多属性,而这些属性的某一部分也在其他页面或模块用到,这时我们就需要对它进行拆分,方便重用。拆分后的两个对象或N个对象,avalon允许我们以ms-controller套ms-controller的形式,实现作用域间的数据共享。换言之,如果某变量在当前的VM换不到,它就会往上找,在上面的VM中查找此属性,一直找到为止。这有点像JS的对象属性查找,其实,它像CSS的作用域查找,因为我们还引入了ms-important。ms-important的寓意就是CSS中的important!符号,就在此作用域查找,不往外找!

    此外,还有些地方,你不想avalon来处理它们,如script标签的内容,style标签的内容,文章的语法高亮部分,引用别人文章的部分,这个可以使用ms-skip指令来绕开这些无用的区域。

    至少,我们学习了ms-controller, ms-important, ms-skip, 更详细可以到新官网上学习

    扫描机制

    avalon能实现VM与视图之间的互动,最关键的东西就是这个。在有的MVVM框架,这也叫做编译(compile),意即,将视图的某一部分的所有指令全部抽取出来,转换为一个个视图刷新函数,然后放到一个个数组中,当VM的属性变动时,就会执行这些数组的函数。当然数组里面的东西不定是函数,也可能是对象,但里面肯定有个视图刷新函数。这是MVVM框架的核心机制,但怎么抽取出来,每个框架的方式都不一样。avalon将这个过程称之为扫描。扫描总是从某个节点开始。在avalon内部,已经默认进行了一次扫描,从body元素开始描。如果我们为页面插入了什么新内容,而这个区域里面又包括了avalon指令,那么我们就需要手动扫描了。

    avalon.scan是avalon第二重要的API,它有两个参数,第一个是元素节点,第二个是数组,里面为一个个VM。当然这两个参数是可选的。但当你手动扫描时,最好都会进去,这样会加快扫描速度,并减少意外。因为所有指令,都扫描后就变移除掉,这包括指定VM用的ms-controller,ms-important!

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <script src="avalon.js"></script>
            <script>
                avalon.ready(function () {
                    var div = document.createElement("div")
                    div.innerHTML = "{{aaa}}"
                    div.setAttribute("ms-controller", "eee")
                    document.body.appendChild(div)
                    var vm = avalon.define({
                        $id: "eee",
                        aaa: 111
                    })
                    avalon.scan(div, vm)
                })
            </script>
        </head>
        <body>
    
        </body>
    </html>
    
    

    指令(绑定)

    指令是指写在HTML中的特殊符号,包括以下几种,ms-开头的绑定属性,写在innerText里面的{{}}的插值表达式,类似data-duplex-xxx的辅助指令(data-后面跟着的绑定属性的名字,它们必须与绑定元素定义在同一元素),还有新添加的自定义标签(它们必须带有:号)

    新手们或从angular过来的人很容易犯一个错误,就是直接在属性值里面加一个{{}},以为就能绑定了,殊不知avalon为了性能优化,会跳过所有非ms-*属性。

    这里拥有所有指令的一览图

    数据填充

    这里提供ms-text, ms-html两种指令,其他ms-text拥有{{expr}}这个变体,ms-html拥有{{expr|html}}这个变体。当你们页面也使用后端模板拼凑而成时,可能 后端会占用了{{}}界定符,我们可以通过以下配置方式重新指定界定符

    avalon.config({
       interpolate:["{%","%}"]
    })
    

    并且我们可以通过avalon.config.openTag, avalon.config.closeTag得到“{%”,"%}"。注意,界定符里面千万别出现<, >,因为这存在兼容性问题。这两个界定符也不能一样,最好它们的长度都大于1。

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

    插值表达式{{}}在绑定属性的使用只限那些能返回字符串的绑定属性,如ms-attr、ms-css、ms-include、ms-class、 ms-href、 ms-title、ms-src等。一旦出现插值表达式,说明这个整个东西分成可变的部分与不可变的部分,{{}}内为可变的,反之亦然。 如果没有{{}}说明整个东西都要求值,又如ms-include="'id'",要用两种引号强制让它的内部不是一个变量。

    模板绑定

    ms-include指令是ms-html的有效补充。我们知道ms-html是将VM中某个符合HTML结构的字符串,放到某元素底下解析为节点。但如果这个字符串很大,放在VM上就不合算,这时我们就想到将它到页面的某个位置上(如script, noscript, textarea等能放大片内容的特殊标签)或干脆独立成一个HTML文件。于是前者叫做内部模板,因为是放在页面的内部,后者叫做外部模板。对于前者,我们使用ms-include=“expr”来引用,后者,我们是使用ms-include-src="expr"来引用。src表示一个路径,因此其值往往是一个URL地址,为了大家方便拼接URL,我们允许ms-include-src的值可以使用插值表达式。如ms-include-src="aaa/{{bbb}}.html"。由于我们加载外部模板时是用AJAX实现的,因此大家在调试代码时,必须打开WEB服务器。

    <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"。

    avalon在使用ms-include-src 加载外部模板时,会将它们存放到avalon.templateCache对象中,因此我们可以搞出一种架构出来,在上线前,将所有要远程加载的模板全部打包到avalon.templateCache对象中,这样它在发出请求前,先查找此对象,发现存在就不会发出请求了。

    注意,无论是ms-include还是ms-include-src都会在其值变化时,请空原元素的所有子孙节点,导致原有数据丢失,里面用到的所有组件重新生成,如果保持原来的节点,可以使用data-include-cache="true"辅助指令。

    下面是一个经典的后台系统框架!

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <script src="avalon.js"></script>
            <script>
                avalon.templateCache = {
                    aaa: "<div>这里是非常复杂的HTML结构1</div>",
                    bbb: "<div>这里是非常复杂的HTML结构2</div>"
                    ccc: "<div>这里是非常复杂的HTML结构3</div>"
                    ddd: "<div>这里是非常复杂的HTML结构4</div>"
                }
                var vm =avalon.define({
                    $id:"root",
                    tabs:["aaa","bbb", "ccc","ddd"],//所有标签页的名字
                    curTab: "aaa",
                    switchTab: function(el){
                        vm.curTab = el
                    },
                    showLoading: function(){},
                    hideLoading:function(){}
                })
            </script>
        </head>
        <body ms-controller="root">
            <table>
                <tr>
                    <td>
                        <ul>
                            <li ms-repeat="tabs" ms-click="switchTab(el)">{{el}}</li>
                        </ul>
                    </td>
                    <td>
                        <!--主内容显示区-->
                        <div ms-include-src="curTab" 
                             data-include-loaded="showLoading"
                             data-include-rendered="hideLoading"
                             >
                        </div>
                    </td>
                </tr>
                
            </table>
        </body>
    </html>
    

    更详细的内容可见新官网

    类名切换(ms-class, ms-hover, ms-active)

    avalon1.5现在只支持新风格,即ms-class="aaa: true"这种形式,此绑定属性的值以冒号分为两部分,前面为类名,后面表示添加或移除。 ms-class="aaa bbb ccc: toggle",当toggle在VM中为true时,它会为元素同时添加aaa, bbb, ccc三个类名。冒号及其后面的东西也不是必须的, 如ms-class="aaa1 bbb2",表示总是为元素添加aaa1,bbb2这两个类名。前面的部分也可以使用插值表达式动态生成,如ms-class="{{className}}:true", className在VM是什么,就会为元素添加什么类名。如果你想为元素添加多个类名绑定,可以使用ms-class-1="aaa: true" ms-class-2="bbb:toggle"来添加。

    ms-hover, ms-active与ms-class的用法相同,但它们一个只在鼠标掠过元素上方时添加类名,移走时移除;另一个则在元素获得焦点时(比如点击)添加类名,失去焦点时移除。

    更详细的内容可见新官网

    事件绑定

    我们可以通过ms-on-*为元素绑定各种事件,比如ms-on-click=fn,表示为当前元素绑定点击事件,fn为VM的一个函数。默认地,我们会为fn传入一个参数event,我们已经为它做了兼容处理,因此你在IE下也能使用preventDefault, stopPropagation, pageX, pageY, target, timeStamp, which等标准属性与方法。如果你还想传其他参数,还想用事件对象,可以用$event占位。ms-on-click=fn(aaa, bbb, $events)。此外,我们为所有常用事件做了快捷处理,因此你们还可以这样用,ms-click=fn2, ms-mouseover=fn3, ms-mouseleave=fn4。 注意,事件绑定的值的第一个单词必须是VM中的函数名字,你不能在其值里面使用加减乘除,如 ms-click="aaa+bbb",这样是不对的。如果 你想同时绑定多个点击事件,用法与ms-class,ms-hover一样,在后面加数字就行了。ms-click-1=fn1 ms-click-2=fn2 ms-click-3=fn3。

    <!DOCTYPE HTML>
    <html>
        <head>
            <title>ms-on</title>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 
            <script src="../avalon.js" ></script>
            <script>
                var model = avalon.define({
                    $id: "test",
                    firstName: "司徒",
                    array: ["aaa", "bbb", "ccc"],
                    argsClick: function(e, a, b) {
                        alert([].slice.call(arguments).join(" "))
                    },
                    loopClick: function(a, e) {
                        alert(a + "  " + e.type)
                    },
                    status: "",
                    callback: function(e) {
                        model.status = e.type
                    },
                    field: "",
                    check: function(e) {
                        model.field = this.value + "  " + e.type
                    },
                    submit: function() {
                        var data = model.$model
                        if (window.JSON) {
                            setTimeout(function() {
                                alert(JSON.stringify(data))
                            })
                        }
                    }
               })
    
            </script>
        </head>
        <body>
            <h3 style="text-align: center">ms-on-*</h3>
            <fieldset ms-controller="test">
                <legend>有关事件回调传参</legend>
                <div ms-mouseenter="callback" ms-mouseleave="callback">{{status}}<br/>
                  <input ms-on-input="check"/>{{field}}
                </div>
            <div ms-click="argsClick($event, 100, firstName)">点我</div>
                <div ms-each-el="array" >
                    <p ms-click="loopClick(el, $event)">{{el}}</p>
                </div>
                <button ms-click="submit">点我</button>
            </fieldset>
        </body>
    </html>
    

    <!DOCTYPE HTML>
    <html>
        <head>
            <title>ms-on</title>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 
            <script src="../avalon.js" ></script>
            <script>
                var count = 0
                var vm = avalon.define({
                    $id: "multi-click",
                    str1: "1",
                    str2: "2",
                    str3: "3",
                    click0: function () {
                        vm.str1 = "xxxxxxxxx" + (count++)
                    },
                    click1: function () {
                        vm.str2 = "xxxxxxxxx" + (count++)
                    },
                    click2: function () {
                        vm.str3 = "xxxxxxxxx" + (count++)
                    }
                })
            </script>
        </head>
        <body>
            <fieldset>
                <legend>一个元素绑定多个同种事件的回调</legend>
                <div ms-controller="multi-click">
                    <div ms-click="click0" ms-click-1="click1" ms-click-2="click2" >请点我</div>
                    <div>{{str1}}</div>
                    <div>{{str2}}</div>
                    <div>{{str3}}</div>
                </div>
            </fieldset>
        </body>
    </html>
    

    一个综合了ms-click, ms-class, 监控数组的例子:

    <!doctype hmtl>
    <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
            <title>attribute</title>
            <script type="text/javascript" src="avalon.modern.js"></script>
            <script>
                var vm = avalon.define({
                    $id: "test",
                    selected: [],
                    array: avalon.range(0, 12),
                    onClick: function (i) {
                        if (vm.selected.indexOf(i) === -1) {
                            vm.selected.push(i)
                        } else {
                            avalon.Array.remove(vm.selected, i)
                        }
                    }
                })
            </script>
            <style>
                b{
                    background: gray;
                    -moz-border-radius: 15px;      /* Gecko browsers */
                    -webkit-border-radius: 15px;   /* Webkit browsers */
                    border-radius:15px;            /* W3C syntax */
                    30px;
                    height:30px;
                    display: inline-block;
                    line-height: 30px;
                    text-align: center;
                }
                b.toggle{
                    background: red;
                }
            </style>
        </head>
    
        <body ms-controller="test">
            <p ms-each="array">
                <b ms-click="onClick($index)" ms-class="toggle: selected.indexOf($index)!==-1">{{$index}}</b> 
            </p>
        </body>
    </html>
    

    更详细的内容可见新官网

    显示绑定(ms-visible)

    ms-visible="expr"是通过改变元素的style.display值来控制元素的显示隐藏。

    在1.5中结合动画指令还可以使用动画效果。

    <!DOCTYPE HTML>
    <html>
        <head>
            <title>ms-visible</title>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 
            <script src="../avalon.js" ></script>
            <script>
                avalon.define({
                    $id: "test",
                    a: true
                })
            </script>
            <style>
                .rect{
                    100px;
                    height:100px;
                    background: red;
                    display: none;
                    border:1px solid red; 
                    text-align: center
                }
            </style>
        </head>
        <body>
            <h3>ms-visible</h3>
            <div ms-controller="test">
                <p>点我隐藏或显示下面的方块<input ms-duplex-checked="a"  type="radio"></p>
                <div class="rect" ms-visible="a" >visible</div>
            </div>
        </body>
    </html>
    

    更详细的内容可见新官网

    插入绑定(ms-if)

    ms-if 与 ms-visible的效果很像,都是不显示元素,但ms-if更进一步,将元素移出原来的位置藏到别处。并且它还会延迟其内容元素的扫描与绑定,因此相对于ms-visible性能更好。如果你想ms-if 与ms-repeat用在同一元素,让某个不合格的数组元素代表的那个地方不显示出来,那你请务必使用ms-if-loop!

    在1.5中结合动画指令还可以使用动画效果。

    <!DOCTYPE HTML>
    <html>
        <head>
            <title>ms-if</title>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 
            <script src="../avalon.js" ></script>
            <style>
                .active{
                    background: blueviolet;
                }
            </style>
            <script>
                var vm = avalon.define({
                    $id: "test",
                    data: {
                        toggle: true
                    },
                    toggle: function () {
                        vm.data.toggle = !vm.data.toggle
                    }
                });
    
            </script>
        </head>
        <body>
            <div class="avalonHide">测试avalonHide</div>
            <div ms-controller="test">
                <div ms-if="data.toggle" class="active">能动态添加移除</div>{{data.toggle}}
                <p><button type='button' ms-click='toggle'>点我</button></p>
            </div>
        </body>
    </html>
    

    更详细的内容可见新官网

    双工绑定(ms-duplex)

    MVVM框架中最重要的功能之一,唯一一个能让视图同步VM的指令。值得注意的是,它绑定的元素只是 表单元素,input, textarea, select。

    在avalon1.5中,ms-duplex只支持duplex2.0。所谓的duplex2.0是指, 我们可以从表单元素的checked属性,value属性抽取数据,转变成所需要的字符串类型,数值类型,布尔类型或数组类型,或添加更多拦截器进行更多操作。

    ms-duplex其实是默认使用了string 拦截器,即相当于ms-duplex-string。

    其他内置拦截器分别是ms-duplex-number, ms-duplex-boolean, ms-duplex-checked

    名字应用元素效果
    ms-duplex-checkedradio, checkbox将其checked值同步到VM
    ms-duplex-string所有表单元素将其value值同步到VM,
    对于checkbox, select[multiple=true]会进一步转换为数组
    ms-duplex-number所有表单元素将其value值同步到VM,
    对于checkbox, select[multiple=true]会进一步转换为数组;
    我们还可以通过data-duplex-number='strong|medium|weak'辅助指令来转换非数值
    为strong时,不能转的都转换为0;
    为medium时,空字符串会忽略转换,否则转换0或其他数字;
    为weak时,不是数值形式的值不做转换
    ms-duplex-boolean所有表单元素将其value值同步到VM,
    对于checkbox, select[multiple=true]会进一步转换为数组;
    转换方式为,当其值为“true”转换为true,其他都转换为false

    默认地,对于文本域,文本区,密码域,框架是使用oninput事件进笨监听,即用户每改动一个字符都会同步到VM。如果想在失去焦点时同步VM,可以使用 data-duplex-event="change"辅助指令来调整。

    另,我们还可以为元素添加一个辅助指令, data-duplex-changed="fn", fn为VM中的一个函数。当元素的值 改动时,它会为回调传入当前的值及回调内部的this指向当前表单元素。

    我们还可以在avalon.duplexHooks上添加自己的拦截器。

    <!DOCTYPE html>
    <html>
        <head>
            <title>ms-duplex</title>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 
            <script src="../avalon.js" ></script>
            <script>
                var model = avalon.define({
                    $id: "test",
                    text: "xxx",
                    textcb: "",
                    textchange: function(a) {
                        model.textcb = a
                    },
                    radio: "radio",
                    radiocb: "",
                    radiochange: function(a) {
                        model.radiocb = a
                    },
                    select: "2222",
                    selectcb: "",
                    selectchange: function(a) {
                        model.selectcb = a
                    }
                })
            </script>
        </head>
        <body ms-controller="test">
            <h3>data-duplex-changed回调</h3>
            <div><input ms-duplex="text" data-duplex-changed="textchange">{{textcb}}</div>
            <div><input ms-duplex-checked="radio" type="radio" data-duplex-changed="radiochange">{{radiocb}}</div>
            <div>
                <select ms-duplex="select" data-duplex-changed="selectchange">
                    <option>1111</option>
                    <option>2222</option>
                    <option>3333</option>
                    <option>4444</option>
                </select>
                {{selectcb}}</div>
        </body>
    </html>
    

    我们可以通过以下方式定义一个拦截器,在avalon.duplexHooks上添加一个对象,此对象拥有init, get, set这三个方法,此三个方法不一定要写全,只要某一个就行了。init在初始化时调用,get方法是同步到VM时调用,set方法是同步视图时调用,这些方法都有两个参数,第一个参数是当前值,第二个参数是绑定对象。

    有了拦截器,我们就可以实现各种表单验证,并且一个ms-duplex是可以带N多拦截器的,如ms-duplex-number-notempty-gt10

    <!DOCTYPE html>
    <html>
        >head>
            <title>TODO supply a title</title>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width">
            <script src="../avalon.js" ></script>
            <script>
                var model = avalon.define({
                    $id: "test",
                    a: 111
                });
                avalon.duplexHooks.add100 = {
                    get: function (val) {
                        return val
                    },
                    set: function (val) {
                        return val + 100
                    }
                }
    
            </script>
        </head>
        <body>
            <div ms-controller="test">
                <input ms-duplex-number-add100="a" >{{a}}
            </div>
        </body>
    </html>
    

    avalon的oniui的验证组件也是基于拦截器机制构建的,大家可以参考一下,方便自己设计自己的表单验证。

    更详细的内容可见新官网

    样式绑定(ms-css)

    用法为ms-css-name="value"。其值可以使用插值表达式, 如ms-css-width=”prop”(会自动补px),ms-css-height=”{{prop}}%”, ms-css-color=”prop”, ms-css-background-color=”prop”, ms-css-background-image=”url({{imageUrl}})”, ms-css-font-size=”{{prop}}px”

    注意:属性值不能加入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 : "#F3F9"
                })
            </script>
            <style>
                .outer{
                    200px;
                    height: 200px;
                    position: absolute;
                    top:1px;
                    left:1px;
                    background: red;
                    z-index:1;
                }
                .inner{
                    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)

    为当前元素添加data-*属性。可以参见ms-attr的用法。

    更详细的内容可见新官网

    属性绑定(ms-attr)

    其用法为ms-attr-name="value", value可以使用插值表达式。当value为undefined, null, false,会移除此属性,否则会添加此属性。

    <!DOCTYPE html>
    <html>
        <head>
            <title>ms-attr-*</title>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width">
            <script src="../avalon.js"></script>
    
            <script>
                var vm = avalon.define({
                    $id: "test",
                    aaa: true,
                    bbb: "@@@",
                    ccc: "&&&",
                    active: "active",
                    click: function(){
                        vm.aaa = !vm.aaa
                    }
                })
               
      
            </script>
            <style>
                .active {
                    background: goldenrod;
                }
                .readonly{
                    border:1px solid blueviolet;
                }
            </style>
    
        </head>
        <body>
            <form method="get" action="aaa.html" ms-controller="test">
                <input ms-enabled="aaa" name="a1" value="12345"/>
                <input ms-disabled="aaa" name="a2" value="67890"/>
                <input ms-readonly="aaa" name="a3" ms-class="readonly: aaa" value="readonly" />
    
                <input ms-duplex-radio="aaa" type="checkbox" value="checkbox" name="a4"/>
                <select name="a5">
                    <option>222</option>
                    <option ms-selected="aaa">555</option>
                </select>
                <p>
                    <input ms-attr-value="其他内容  {{ccc}}" name="a6" value="改"/>
                    <input ms-attr-value="'其他内容 '+ccc" name="a7" value="改" />
                    <input ms-value="其他内容  {{ccc}}" name="a8" value="改"/>
                </p>
                <button type="button" ms-click="click" ms-attr-class="active">
                    点我
                </button>
                <input type="submit" value="提交" />
                <svg width="100" height="100">
                <circle ms-attr-cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
                </svg>
            </form>
        </body>
    </html>
    

    更详细的内容可见新官网

    循环绑定(ms-repeat, ms-each, ms-with)

    这三个东西都很相像,见下表

    名字循环的类型循环的范围
    ms-repeat用于循环数组与对象循环当前元素
    ms-each用于循环数组循环当前元素的内部
    ms-with用于循环对象循环当前元素的内部

    当我们用ms-each, ms-repeat循环数组时,可以为元素指定其别名,如ms-each-item="array1", ms-repeat-elem="array2", 如果你不指定别名,则默认为el。(注意,别名不能出现大写,因为属性名在HTML规范中,会全部转换为小写,详见这里)。监控数组拥有原生数组的所有方法,并且比它还多了set, remove, removeAt, removeAll, ensure, pushArray与 clear方法 。详见这里

    注意,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就有$val, $key, $index, $outer等属性。$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 src="avalon.js"></script>
            <style>
                .id2013716 {
                     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>
        

    更详细的内容可见新官网

    动画绑定(ms-effect)

    avalon1.5新增的功能,允许我们用avalon.effect方法定义一个动画,然后在页面上存在ms-if,ms-repeat,ms-include,ms-visible的元素添加ms-effect="effectName"实现动画效果。此动画可以使用CSS3与JS实现。

    更详细的内容可见新官网

    自定义标签组件

    通过自定义标签声明使用某个组件,这是avalon1.5引入的最重要特性。

    早在20年前,JAVA也想通过标签库实现堆积木般的开发。但鉴于当时JAVAer的技术力量,这东西没有做成;现在谷歌有了自己的浏览器,实现了一套WEB Components规范,开源polymer,也是往这条路子走下去。因为以标签的形式声明组件比起在JS里以类的形式创建组件,来得更简单明了。

    <oni:buttonset>
        <oni:button color="danger"  type="icon" icon-position="left" icon=""></oni:button>
        <oni:button color="danger"  type="icon" icon-position="left" icon=""></oni:button>
        <oni:button color="danger"  type="icon" icon-position="left" icon=""></oni:button>
    </oni:buttonset>
    

    做这套东西有几个难点:

    • 生命周期管理,从构建配置对象,到模板微调,到初始化,到子组件就绪,到自身组件就绪,到组件销微,都有自己相应的回调。
    • 继承机制
    • 当前组件必须等到子组件就绪才闭合自身,比如父组件的高度是由子组件决定
    • 组件的某些很大块的区块如何定义(这个引入插入点的概念处理),如弹出层有title, content, footer

    为了让一个页面使用多个UI库,avalon引入了命名空间的概念,这是来自早期XML的设计。一个标签名由冒号隔开,前面是命名空间,后面是标签名。

    avalon.library("oni", {})//这是声明一个命名空间,也是算声明一个UI库
    

    在定义一个UI组件,最少有3个文件,JS文件(引用其他两个文件或更多子组件的JS),HTML文件(组件的模块),CSS文件(它可以由SASS更高级的语言生成)。

    
    define(["avalon",
        "text!./avalon.pager.html",
        "css!../chameleon/oniui-common.css",
        "css!./avalon.pager.css"
    ], function (avalon, template) {
    
        var _interface = function () {
        }
        avalon.component("oni:pager", {
            regional: {}, //@config {Object} 默认语言包
            perPages: 10, //@config {Number} 每页包含多少条目
            showPages: 10, //@config {Number} 中间部分一共要显示多少页(如果两边出现省略号,即它们之间的页数) 
            currentPage: 1, //@config {Number} 当前选中的页面 (按照人们日常习惯,是从1开始),它会被高亮 
            _currentPage: 1, //@config {Number}  跳转台中的输入框显示的数字,它默认与currentPage一致
            totalItems: 200, //@config {Number} 总条目数
            totalPages: 0, //@config {Number} 总页数,通过Math.ceil(vm.totalItems / vm.perPages)求得
            pages: [], //@config {Array} 要显示的页面组成的数字数组,如[1,2,3,4,5,6,7]
            ellipseText: "…", //@config {String} 省略的页数用什么文字表示 
            prevText: "<", //@config {String} “下一页”分页按钮上显示的文字 
            nextText: ">", //@config {String} “上一页”分页按钮上显示的文字 
            firstPage: 0, //@config {Number} 当前可显示的最小页码,不能小于1
            $ready: function(){
                  //....
           }
           //....
       })
    })
    

    avalon.component是用来定义一个组件,第一个参数为自定义标签的名字(包括命名空间),第二个是配置对象,里面定义这个组件用到的属性,状态与方法。

    如何做一个avalon组件

    更详细的内容可见新官网

    我们也可以到这里直接看pager, button, checkboxlist是怎么定义。

    未来与avalon配套使用的OniUI一点点全部改成自定义标签的形式,更希望大家一起为avalon设计更多的UI组件。

    模块间通信及属性监控 $watch,$fire

    avalon1.5是无比强大,并现在只有VM第一层有$watch, $fire方法,并去掉$unwatch方法。$watch方法会返回一个方法,调用它就会去掉此回调。

    早期$watch只能监听当前层的属性,不能监听其子对象的属性,尤其在数组中,无法监听元素的属性变动。因此在avalon1.5中,引入通配符及多级监听功能。

    var vm = avalon.define({
         $id: "test",
         array: [1, 2, 3],
         arr: [{a: 1}, {a: 2}, {a: 3}],
         obj: {
             a: 1,
             b: 2
         },
         a: {
             b: {
                 c: {
                     d: 33
                 }
             }
         }
     })
     vm.$watch("array.*", function (a, b) {
         expect(a).to.be(6)
         expect(b).to.be(2)
     })
     vm.$watch("arr.*.a", function (a, b) {
         expect(a).to.be(99)
         expect(b).to.be(1)
     })
     vm.$watch("obj.*", function (a, b, c) {
         expect(a).to.be(111)
         expect(b).to.be(1)
     })
     vm.$watch("a.*.c.d", function (a, b, c) {
         expect(a).to.be(88)
         expect(b).to.be(33)
     })
     setTimeout(function () {
         vm.array.set(1, 6)
         vm.arr[0].a = 99
         vm.obj.a = 111
         vm.a.b.c.d = 88
     }, 100)
    

    注意,*通配任只能出现一次

    var unwatch = vm.$watch("array.*", function (a, b) {
        expect(a).to.be(6)
        expect(b).to.be(2)
     })
    unwatch() //移除当前$watch回调
    

    如果在跨模块通信,由于所有VM都储存在avalon.vmodels中,我们可以遍历它们,找到目标VM,然后$fire。

    avalon多级联动例子:

    <!DOCTYPE html>
    <html>
        <head>
            <title>三级联动</title>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <script src="avalon.js"></script>
    
            <script>
                var map = {
                    "中国": ["江南四大才子", "初唐四杰", "战国四君子"],
                    "日本": ["日本武将", "日本城堡", "幕府时代"],
                    "欧美": ["三大骑士团", "三大魔幻小说", "七大奇迹"],
                    "江南四大才子": ["祝枝山", "文征明", "唐伯虎", "周文宾"],
                    "初唐四杰": ["王勃", "杨炯", "卢照邻", "骆宾王"],
                    "战国四君子": ["楚国春申君黄歇", "齐国孟尝君田文", "赵国平原君赵胜", "魏国信陵君魏无忌"],
                    "日本武将": ["织田信长", "德川家康", "丰臣秀吉"],
                    "日本城堡": ["安土城", "熊本城", "大坂城", "姬路城"],
                    "幕府时代": ["镰仓", "室町", "丰臣", "江户"],
                    "三大骑士团": ["圣殿骑士团", "医院骑士团", "条顿骑士团"],
                    "三大魔幻小说": ["冰与火之歌", "时光之轮", "荆刺与白骨之王国"],
                    "七大奇迹": ["埃及胡夫金字塔", "奥林匹亚宙斯巨像", "阿尔忒弥斯月神殿", "摩索拉斯陵墓", "亚历山大港灯塔", "巴比伦空中花园", "罗德岛太阳神巨像"]
                }
                var vm = avalon.define({
                    $id: "test",
                    first: ["中国", "日本", "欧美"],
                    second: [],
                    third: [],
                    firstSelected: "日本",
                    secondSelected: "日本武将",
                    thirdSelected: "织田信长"
                })
    
                vm.second = map[vm.first[1]].concat()
                vm.third = map[vm.second[0]].concat()
    
                vm.$watch("firstSelected", function (a) {
                    vm.second = map[a].concat()
                    vm.secondSelected = vm.second[0]
                })
                vm.$watch("secondSelected", function (a) {
                    vm.third = map[a].concat()
                    vm.thirdSelected = vm.third[0]
                })
            </script>
        </head>
        <body>
            <div ms-controller="test">
                <h3>下拉框三级联动</h3>
                <select ms-duplex="firstSelected" >
                    <option  ms-repeat="first" ms-attr-value="el" >{{el}}</option>
                </select>
                <select ms-duplex="secondSelected" >
                    <option ms-repeat="second" ms-attr-value="el" >{{el}}</option>
                </select>
                <select ms-duplex="thirdSelected" >
                    <option ms-repeat="third" ms-attr-value="el" >{{el}}</option>
                </select>
            </div>
        </body>
    </html>
    

    过滤器

    avalon从angular中抄来管道符风格的过滤器,但有点不一样。 它只能用于{{}}插值表达式。如果不存在参数,要求直接跟|filter,如果存在参传,则要用小括号括起,参数要有逗号,这与一般的函数调用差不多,如|truncate(20,"……")

    avalon自带以下几个过滤器

    html
    没有传参,用于将文本绑定转换为HTML绑定
    sanitize
    去掉onclick, javascript:alert等可能引起注入攻击的代码。
    uppercase
    大写化
    lowercase
    小写化
    truncate
    对长字符串进行截短,truncate(number, truncation), number默认为30,truncation为“...”
    camelize
    驼峰化处理
    escape
    对类似于HTML格式的字符串进行转义,把尖括号转换为&gt; &lt;
    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)
          '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.filters.default = function(str, defaultStr){
                    return str || defaultStr
                 }
               //性别过滤器
                 avalon.filters.sex = function(str){
                     return str == '0' ? '男': '女'
                  }
    
        

    自定义指令(绑定)

    avalon1.5新添加的API,avalon.directive, 用于快速创建一种全新的指令!

    过去avalon自定义绑定比较麻烦,非常影响avalon推广,也不利于业务上各种功能的开发,新的定义方式将变得非常简单

    avalon.directive(name, {
      init: function(binding){
         //在这里处理binding.expr属性
      },
      update: function(value, oldValue){
      //value是binding.expr经过解析得到的当前VM属性的值
      //oldValue是它之前的值
      },
    })
    

    拿最简单的data绑定来说吧:

    avalon.directive("data", {
        priority: 100,
        update: function (val) {
            var elem = this.element
            var key = "data-" + this.param
            if (val && typeof val === "object") {
                elem[key] = val
            } else {
                elem.setAttribute(key, String(val))
            }
        }
    })
    

    我们发现它没有init回调,init主要是对binding.expr进行加工,或绑定事件的,它这些都不需要, 就会直接返回原始值.比如说,<div ms-data-xxx="yyy"< 这里的属性会抽取成下面一个对象:

    var binding = {
       name: "ms-data-xxx",
       expr: "yyy",
       type: name,
       element: DIVElement,
       param: "xxx",
       oneTime: false,
       uuid: //框架会在这里生成一个UUID给它
       priority://框架会根据上面的定义,计算出来,大概是1000+ ;
    }
    

    假如此时vm.yyy = 999; 那么第一次update时,value与oldValue分别为 999, undefined

    对于用户来说,priority一般不需要设置, 其计算公式为

    //详见 scanAttr
    priority: (directives[type].priority || type.charCodeAt(0) * 10) + (Number(param.replace(/D/g, "")) || 0)
    
    

    此外,如果你的绑定需要绑定一些事件来监听用户行为,那么你得在init或update添加一个roolback回调,在里面解绑定事件.

    update方法中的this就是init的传参binding

    那么我们做一下简单的例子吧:

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <script src="avalon.js">
    
            </script>
            <script>
                avalon.directive("foo", {
                    init: function (binding) {
                        var elem = binding.element
                        var vmodels = binding.vmodels
                        var remove = avalon(elem).bind("click", function () {
                            elem.innerHTML = new Date - 0
                            for (var i = 0, v; v = vmodels[i++]; ) {
                                if (v.hasOwnProperty(binding.expr)) {
                                    v[binding.expr] = elem.innerHTML
                                    break
                                }
                            }
    
                        })
                        binding.roolback = function () {
                            avalon(elem).unbind("click", remove)
                        }
                    },
                    update: function (value, oldValue) {
                        this.element.innerHTML = value
                    }
                })
                var vm = avalon.define({
                    $id: "test",
                    aaa: 111
                })
                vm.$watch("aaa", function (a, b) {
                    console.log(a, b)
                })
            </script>
        </head>
        <body>
            <div ms-controller="test">
                <div ms-foo="aaa">点我</div>
            </div>
        </body>
    </html>
    

    别看例子简单,其实它是除duplex外又一个新的双工绑定!

    加载器

    avalon自带加载器,它是使用业界最通用的AMD规范。

    如果你想禁用自带加载器,有两种办法:

    • 先入avalon再引入其他加载器,然后立即禁用avalon自带加载器
      avalon.config({
             loader:false
      })
                 
    • 这里下载没有加载器的版本(以shim结尾)

    至于avalon是怎么与jQuery, requirejs怎么搭配,怎么打包,可以详看这里

    AJAX

    avalon本身没提供AJAX模块,大家可以使用jQuery或 mmRequest

                require(["mmRequest/mmRequest"], function() {
                    var vm = avalon.define("test", function(vm) {
                        vm.loadScript = function() {
                            avalon.getScript("test.js", function() {
                                console.log("success")
                                vm.time = + new Date()
                            })
                        }
                        vm.loadAJAX = function() {
                            avalon.ajax({
                                url: "test.js",
                                dataType: "script",
                                data: {
                                    page: 1,
                                    name: 1
                                },
                                success: function() {
                                    console.log("success")
                                    vm.time = + new Date()
                                }
                            })
                        }
                        vm.jsonData = []
                        vm.time = ""
                    })
                    avalon.scan()
                })
    

    mmRequest覆盖原jQuery所有的功能,其文档地址在这里

    如果我们想将VM提交到后台,由于VM存在许多方法与其他东西,直接提交数据量很大,因此我们可以通过下面方法提交纯数据

    $("button").keyup(function(){
      $.post("demo_ajax_gethint.asp", JSON.parse(JSON.stringify(vm.$model)),function(result){
          alert("提交成功")
      });
    });
    

    路由系统

    avalon提供了mmRouer与mmState两个路由器,但mmState也是基于mmRouter开发的,引入mmState就会自动引入mmRouter。

    这里两个加载器都可以在这里下载到,包括(mmPromise, mmHIstory, mmRouter, mmState)

    其文档放在这里,有不懂的地方直接在此仓库提ISSUE

    在IE6下调试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})

    avalon1.4.6到1.5的升级指南

    • avalon1.5不再支持旧风格,avalon.define(id, callback)全部改成avalon.define(object)这种形式
    • avalon1.5定义计算属性时,需要在$computed对象上统一定义
    • avalon1.5生成的VM 只有第一层对象上拥有 $fire, $watch方法,子对象没有$fire, $watch方法。之前监听数组长度变化时,是用vm.array.$watch("length",callback),现在改成vm.$watch("array.length",callback),之前监听子对象某个属性的变化,是使用vm.aaa.$watch("bbb",callback),现在改成vm.$watch("aaa.bbb",callback),之前监听某一层的所有属性变化是vm.$watch("all!", callback),现在改成vm.$watch("*",callback);之前不能监听数组元素变化,现在可以vm.$watch("array2.*", callback)
    • avalon1.5不再支持$unwatch,详见上方。
    • avalon1.5不再支持data-duplex-focus, data-duplex-observe辅助指令
    • avalon1.5不再支持ms-widget,请使用avalon.component创建组件,然后以自定义标签的形式声明使用。
    • avalon1.5不再使用avalon.bindingHandlers,avalon.bindingExecutors来创建新指令,再改用更方便的avalon.directive方法
    • avalon在循环对象时,以前需要自己计算索引值,现在它与循环数组一样,存在$index变量了!

    其他要注意的地方(更新VM等)

    avalon是通过Object.defineProperty来实现属性监听,因此不像angular,每次都VM的所有属性都比较一遍,而是用户一改动属性就立即触发视图变动。

    这是avalon的优势,但隐藏一个不好的地方。用户必须在avalon.define的配置对象将所有监听的对象预先写好,否则无法监听到。

               var vm =  avalon.define({
                    $id: "test",
                    a: "aaa", //这个可以监听,改变它会同步视图
                    $b:"ccc"  //这个是非监控属性,改变它不会同步视图
                })
                vm.c = '111' //这是avalon.define后才添加的,改变它不会同步视图
                             //在IE6-8还会抛错
    

    avalon在IE6-8是使用VBScript实现,不支持随便添加新属性,比如上例就会抛错

    VBScript还带一个问题,它是不区分大小写,aaa与AAA是认为一样的,这时它会报重复定义的问题。

    某些属性由于是VBscript的关键字,也不能作为属性或方法,否则在IE6-8也抛错,如type, err, erm, me

    由于avalon是通过Object.defineProperty来实现属性监听,它会转换其底下的每一个属性,每一个数组,每一个子对象,因此对象的层次不太深,否则会带来性能问题。最好不超过3层,数组的层次也宜过长。并且尽量使用$开头或放在$skipArray减少无谓的属性监听。

    avalon可以通过以下手段撕开不能监听新属性的限制,就是重写子对象!

               var vm =  avalon.define({
                    $id: "test",
                    a: {}
                })
                vm.a = {
                   b: 2,//现在a.b, a.c, a.d可以与视图互相同步了
                   c: 3
                   d: 4
                }
    

    如果你想重写数组的某一个元素,可以其set方法,如果交互两个数组元素,请使用splice方法或深拷贝原数组再赋值。

    <!DOCTYPE html>
    <html lang="zh-cn">
        <head>
            <meta charset="utf-8">
            <script src="avalon.js"></script>
            <script>
                var vm = avalon.define({
                    $id: "test",
                    items: [
                        {
                            name: 'First lady'
                        },
                        {
                            name: 'Second boy'
                        },
                        {
                            name: 'Third guy'
                        }
                    ];
                });
    
                var temp = vm.items.splice(2, 1);
                vm.items.splice(1, 0, vm[0].$model);//如果是简单数据类型,就不需要.$model
            </script>
        </head>
        <body>
            <ul ms-controller="test">
                <li ms-repeat="items">{{ el.name }}</li>
            </ul>
        </body>
    </html>
    

    <!DOCTYPE html>
    <html>
    
        <head>
            <title>avalon入门</title>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
            <script src="avalon.js" type="text/javascript"></script>
            <script>
                var model = avalon.define({
                    $id: 'test',
                    arr: [{
                            'value': '0',
                            'text': 'zero'
                        }, {
                            'value': '1',
                            'text': 'one'
                        }, {
                            'value': '2',
                            'text': 'two'
                        }, {
                            'value': '3',
                            'text': 'three'
                        }],
                    click: function () {
    
    
                        var aa = avalon.mix(true, [], model.$model.arr)
                        //交换one和three
    
                        aa[1] = aa[3]
                        aa[3] = aa[1]
    
                        //控制台显示交换成功
                        console.log(aa[1])
                        console.log(aa[3])
                        model.arr = aa
                    }
                })
            </script>
        </head>
    
        <body>
            <div ms-controller="test">
                <button type="button" ms-click="click">xxx</button>
                <ul>
                    <li ms-repeat="arr" >
                        {{el.text}}
                    </li>
                </ul>
            </div>
        </body>
    
    </html>
    

    与mmState搭配使用时,页面上的组件重复生成的解决方法:在ms-view所在元素上添加一属性data-view-cache="true", 。但如果两个视图有相同的dialog(就是ms-include-src='路径.html')相互之前切换的话dialog vmodels会被删除。就把调用公用dialog的html移除到ms-view之外解决了。

    我们可以在官网这里看到更多有用的信息

    有问题请到GITHUB提ISUUE,或到avalon专门论坛提问。

    avalon在慕课网也有相关视频教程前端乱炖上有系列长文剖析avalon的原理

    本人也承受北京地区的avalon上门培训!!!

  • 相关阅读:
    解决struts2在(IE,Firefox)下载文件名乱码问题
    Quartz 使用
    SpringBoot 全局异常处理器
    数据结构学习(五) Java链表实现队列
    数据结构学习(四) Java链表实现
    Linux 端口占用情况查看
    数据结构学习(三) 基于动态数组实现队列Queue
    数据结构学习(二)基于动态数组实现Stack(栈)
    SpringBoot 定时任务
    数据结构学习(一) Java中的动态数组实现
  • 原文地址:https://www.cnblogs.com/rubylouvre/p/4783966.html
Copyright © 2011-2022 走看看