csjoz11

导航

迷你MVVM框架 avalonjs 入门教程

  1. 关于AvalonJs
  2. 开始的例子
  3. 扫描
  4. 视图模型
  5. 数据模型
  6. 绑定
  7. 作用域绑定(ms-controller, ms-important)
  8. 忽略扫描绑定(ms-skip)
  9. 模板绑定(ms-include)
  10. 数据填充(ms-text, ms-html)
  11. 类名切换(ms-class, ms-hover, ms-active)
  12. 事件绑定(ms-on,……)
  13. 显示绑定(ms-visible)
  14. 插入绑定(ms-if)
  15. 双工绑定(ms-duplex)
  16. 样式绑定(ms-css)
  17. 数据绑定(ms-data)
  18. 属性绑定(ms-attr)
  19. 循环绑定(ms-repeat)
  20. 数组循环绑定(ms-each废弃)
  21. 对象循环绑定(ms-with废弃)
  22. UI绑定(ms-widget)
  23. 模块间通信及属性监控 $watch,$fire, $unwatch
  24. 过滤器
  25. AMD加载器
  26. 路由系统
  27. AJAX
  28. 功能扩展
  29. 在IE6下调试avalon
  30. 权限控制

关于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与监控数组实现一个树

注意,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-data实现事件代理的技巧,建议事件绑定接口支持事件代理,最简单就是table上可以绑定td的点击事件

显示绑定(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"], function() {
                avalon.define({
                    $id: "test",
                    $opts: {
                        name: "这是控件的内容"
                    }
                })
                avalon.scan()
            })
        </script>
        <div ms-controller="test" ms-widget="testui,ddd,$opts" >
            <input ms-duplex="value" />
            <button type="button" ms-click="plus">ClickMe</button>
        </div>
    </body>
</html>

如果你想拿到组件的VM,可以使用onInit回调实现, 详情见这里   或者这里

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

avalon内置了一个强大的自定义事件系统,它在绑定在每一个VM上。每一个VM都拥有$watch, $unwatch, $fire这三个方法,及一个$events对象。$events是用于储存各种回调。先从单个VM说起,如果一个VM拥有aaa这个属性,如果我们在VM通过$watch对它监控,那么当aaa改变值时,它对应的回调就会被触发!

var vmodel = avalon.define({
     $id: "test",
     aaa: 111
 })
  vmodel.$watch("aaa", function(newValue, oldValue){
       avalon.log(newValue) //222
       avalon.log(oldValue) //111
   })
 setTimeout(function(){
     vmodel.aaa = 222
 }, 1000)

注意,它只能监听当前属性的变动。

我们还可以通过$unwatch方法,移除对应的回调。如果传入两个参数,第一个是属性名,第二个是回调,那么只移除此回调;如果只传入一个属性名,那么此属性关联的所有回调都会被移除掉。

有时,我们还绑定了一些与属性名无关的事件回调,想触发它,那只能使用$fire方法了。$fire方法第一个参数为属性名(自定义事件名),其他参数随意。

var vmodel = avalon.define({
     $id: "test",
     aaa: 111
 })
 vmodel.$watch("cat", function(){
     avalon.log(avalon.slice(arguments)) //[1,2,3]
  })
 setTimeout(function(){
     vmodel.$fire("cat",1,2,3)
 }, 1000)

更高级的玩法,有时我们想在任何属性变化时都触发某一个回调,这时我们就需要$watch一个特殊的属性了——“$all”。不同的是,$watch回调的参数多了一个属性名,排在最前面。

var vmodel = avalon.define({
     $id: "test",
     aaa: 111,
     bbb: 222,
     
 })
 vmodel.$watch("$all", function(){
     avalon.log(avalon.slice(arguments))
         // ["aaa", 2, 111]
         // ["bbb", 3, 222]
 })
 setTimeout(function(){
     vmodel.aaa = 2
     vmodel.bbb = 3
 }, 1000)

手动触发$fire是位随着高风险的,框架内部是做了处理(只有前后值发生变化才会触发),因此万不得已使用它,但又爆发死循环怎么办?这样就需要暂时中断VM的属性监控机制。使用$unwatch(),它里面什么也不传,就暂时将监控冻结了。恢复它也很简单,使用$watch(),里面也什么也不传!

不过最强大的用法是实现模块间的通信(因为在实际项目中,一个页面可能非常大,有多少人分块制作,每个人自己写自己的VM,这时就需要通过某种机制来进行数据与方法的联动了),这是使用$fire方法达成的。只要在$fire的自定义事件名前添加"up!", "down!", "all!"前缀,它就能实现angular相似的$emit,$broadcast功能。

<!DOCTYPE html>
<html>
    <head>
        <title>by 司徒正美</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <script src="avalon.js"></script>
        <script>
          var vm1 = avalon.define({
                $id: "ancestor",
                aaa : '1111111111',
                click : function() {
                    avalon.log("向下广播")
                    vm1.$fire("down!aaa", "capture")
                }
            })
            vm1.$watch("aaa", function(v) {
                    avalon.log(v)
                    avalon.log("ancestor.aaa事件被触发了")
            })
            var vm2 = avalon.define({
                $id: "parent",
                text : "222222222"
                aaa : '3333333333',
                click : function() {
                    console.log("全局扩播")
                    vm2.$fire("all!aaa", "broadcast")
                }
            })
            vm2.$watch("aaa", function(v) {
                   avalon.log(v)
                   avalon.log("parent.aaa事件被触发了")
             })
            var vm3 = avalon.define(
                $id: "son",
                click : function() {
                    console.log("向上冒泡")
                    vm3.$fire("up!aaa", "bubble")
                }
            })
            vm3.$watch("aaa", function(v) {
                    avalon.log(v)
                    avalon.log("son.aaa事件被触发了")
              })
        </script>
        <style>
 
        </style>
    </head>
    <body class="ms-controller"   ms-controller="ancestor">
        <h3>avalon vm.$fire的升级版 </h3>
        <button type="button" ms-click="click">
            capture
        </button>
        <div ms-controller="parent">
            <button type="button" ms-click="click">broadcast</button>
            <div ms-controller="son">
                <button type="button" ms-click="click">
                    bubble
                </button>
            </div>
        </div>
    </body>
</html>

过滤器

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

avalon自带以下几个过滤器

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

AMD 加载器

avalon装备了AMD模范的加载器,这涉及到两个全局方法 require与define

require(deps, callback)

deps 必需。String|Array。依赖列表,可以是具体路径或模块标识,如果想用字符串表示多个模块,则请用“,”隔开它们。

callback 必需。Function。回调,当用户指定的依赖以及这些依赖的依赖树都加载执行完毕后,才会安全执行它。

模块标识

一个模块标识就是一个字符串,通过它们来转换成到对应JS文件或CSS文件的路径。

有关模块标识的CommonJS规范,可以见 这里

具体约定如下:

  1. 每个模块标识的字符串组成只能是合法URL路径,因此只能是英文字母,数字,点号,斜扛,#号。
  2. 如果模块标识是 以"./"开头,则表示相对于它的父模块的目录中找。
  3. 如果模块标识是 以"../"开头,则表示相对于它的父模块的父目录中找。
  4. 如果模块标识不以点号或斜扛开始,则有以下三种情况
    1. 如果此模块标识在 $.config.alias存在对应值,换言之某一模块定义了一个别名,则用此模块的具体路径加载文件。
    2. 如果此模块标识 以http://、https://、file:/// 等协议开头的绝对路径,直接用它加载文件。
    3. 否则我们将在引入框架种子模块(avalon.js)的目录下寻找是否有同名JS文件,然后指向它。
  5. 对于JS模块,它可以省略后缀名,即“.js”可有可无;但对于CSS需要使用css!插件机制。
  6. 框架种子模块的目录保存于 $.config.base属性中。
  7. ready!是系统占位符,用于表示DOM树是否加载完毕,不会进行路径转换。

如果想禁止使用avalon自带的加载器,可以在第一次调用require方法之前,执行如下代码:

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <script src="require.js"></script>
        <script src="avalon.modern.js"></script>
        <script>
            avalon.config({loader: false})
            alert(require)
            avalon.define("xxx", function(vm){
                vm.aaa = "司徒正美"
            })
        </script>
    </head>
    <body ms-controller="xxx" >
        <div>{{aaa}}</div>
    </body>
</html>

与jquery更好的集成,比如一些旧系统,直接在页面引入jquery库与其大量jquery插件,改成动态加载方式成本非常大。怎么样才能与jquery和平共存,亦能让AMD加载发挥作呢?先引入jquery库, 然后将avalon.modules.jquery 加个预设值(exports: jquery用于shim机制, state: 2 表明它已经加载完毕)就行了。

例子
<!DOCTYPE html>
<html>
 
    <head>
        <title></title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <script src="jquery.js" type="text/javascript"></script>
        <script src="avalon.js" type="text/javascript"></script>
 
    </head>
    <body>
        <div ms-controller="main" ms-click="click">
            <p>
                <a href="#" >点击我</a>
            </p>
        </div>
        <script type="text/javascript">
 
            avalon.modules.jquery = {
                exports: jQuery,
                state: 2
            }
            require(['jquery','domReady!'], function($) {
                avalon.log('加载jq了啊……')
                $.ajaxSetup({
                    headers: {ajaxRequest: true},
                    beforeSend: function(o) {
                        avalon.log(typeof o)
                        avalon.log(typeof o.id)
                    },
                    complete: function(data) {
                        avalon.log('ajax 成功执行啦,阿门!')
                    }
                })
                $('body').bind("click", function(e) {
                    alert("document");
                    avalon.log(typeof e.target.$vmodel)
                    $.post('./h.js', {}, function(res) {
                        avalon.log(typeof res)
                    })
                });
            })
 
        </script>
    </body>
</html>
<!DOCTYPE html>
<html>
 
    <head>
        <title></title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <!-->这里没有东西</-->
        <script src="avalon.js" type="text/javascript"></script>
 
    </head>
    <body>
        <div ms-controller="main" ms-click="click">
            <p>
                <a href="#" >点击我</a>
            </p>
        </div>
        <script type="text/javascript">
/* 0.982之前可以
            avalon.config({
                alias: {
                    jquery: {
                        exports: "jQuery",//这是原来jQuery库的命名空间,必须写上
                        src: "jquery.js"
                    }
                }
            })
*/
//下面是兼容requirejs的方法,推荐使用这个
           avalon.config({
                paths: {
                    jquery: "http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"
                },
                shim: {
                    jquery: {
                        exports: "jQuery"//这是原来jQuery库的命名空间,必须写上
                    }
                }
            })
            require(['jquery','domReady!'], function($) {
                avalon.log('加载jq了啊……')
                $.ajaxSetup({
                    headers: {ajaxRequest: true},
                    beforeSend: function(o) {
                        avalon.log(typeof o)
                        avalon.log(typeof o.id)
                    },
                    complete: function(data) {
                        avalon.log('ajax 成功执行啦,阿门!')
                    }
                })
                $('body').bind("click", function(e) {
                    alert("document");
                    avalon.log(typeof e.target.$vmodel)
                    $.post('./h.js', {}, function(res) {
                        avalon.log(typeof res)
                    })
                });
            })
 
        </script>
    </body>
</html>
例子

加载单个模块。

// 由于lang.js与mass.js是位于同一目录下,可以省略./
require(["lang"], function(lang) {
    alert(lang.String.toUpperCase("aa"))
});
例子

加载多个模块。需要注意的是,涉及DOM操作时必须要待到DOM树建完才能进入,因此我们在这里指定了一个标识,叫"ready!", 它并不一个模块,用户自定义模块,也不要起名叫"ready!"。

require(["jquery","node","attr","domReady!"], function($) {
    alert($.fn.attr + "");
    alert($.fn.prop + "");
});
例子

加载多个模块,使用字符串数组形式的依赖列表。

require(["jquery", "css", "domReady!"], function($, css) {
    $("#js_require_ex3").toggle();
});
例子

加载CSS文件。

require(["jquery", "domReady!", "css!http//sdfds.xdfs.css"], function($) {
    $("#js_require_ex3").toggle();
});
例子

使用别名机制管理模块的链接。

             var path = location.protocol + "//" + location.host + "/doc/scripts/loadtest/"
/* 0.982之前可以
             require.config({
                 alias: {
                     "aaa": path + "aaa.js",
                     "bbb": path + "bbb.js",
                     "ccc": path + "ccc.js",
                     "ddd": path + "ddd.js"
                 }
             })
*/
//下面是兼容requirejs的方法,推荐使用这个
             require.config({
                paths: {
                     "aaa": path + "aaa.js",
                     "bbb": path + "bbb.js",
                     "ccc": path + "ccc.js",
                     "ddd": path + "ddd.js"
                 }
             })
             require(["aaa","bbb","domReady"], function(a, b, $) {
                 var parent = $("#loadasync2")
                 parent.append(a);
                 parent.append(b);
                 $("#asynctest2").click(function() {
                     require(["ccc","ddd"], function(c, d) {
                         parent.append(c);
                         parent.append(d);
                     })
                 })
             });
例子

加载不按规范编写的JS文件,可以让你不用改jQuery的源码就加载它。相当于其他加载器的shim插件。 与别名机制不同的是,现在它对应一个对象,src为完整路径,deps为依赖列表,exports为其他模块引用它时,传送给它们的参数

!function() {
               var path = "https://files.cnblogs.com/shuicaituya/"
               require.config({
                   pashs: {
                       "jquery":  path + "jquery.js"
                   },
                   shim:{
                       jquery:   {
                           deps: [], //没有依赖可以不写
                           exports: "jQuery"
                       }
                  }
               });
               require(["jquery"], function($) {
                   alert($)
                   alert("回调调起成功");
               })
           }()

如果你想用其他AMD加载器,最好的办法还是建议直接打开源码,拉到最底几行,把加载器禁用了!

avalon.config({
    loader: false//原来是true!!!!!!!!!!1
})

define方法用于定义一个模块,格式为:

define( id?, deps?, factory )

 id
 可选。String。模块ID。它最终会转换一个URL,放于 $.modules中。
 deps
 可选。String|Array。依赖列表。
 factory
 必需。Function|Object。模块工厂。它的参数列参为其依赖模块所有返回的值,如果某个模块没有返回值,则对应位置为undefined
    

注意, define方法不能写在script标签的innerHTML中,只能写在JS文件里。

例子

加载不按规范编写的JS文件,可以让你不用改jQuery的源码就加载它。相当于其他加载器的shim插件。 与别名机制不同的是,现在它对应一个对象,src为完整路径,deps为依赖列表,exports为其他模块引用它时,传送给它们的参数

//aaa.js 没有依赖不用改
define("aaa", function() {
    return 1
})
 
//bbb.js  没有依赖不用改
define("bbb", function() {
    return 2
});
//ccc.js
define("ccc", ["$aaa"], function(a) {
    return 10 + a
})
 
//ddd/ddd.js
define("ddd", ["$ddd"], function(c) {
    return c + 100
});

avalon与seajs, https://github.com/RubyLouvre/avalon/issues/313

我们也可以在源码里面直接移除AMD加载器模块

路由系统

它需要依赖于另一个独立的组件mmRouter,用法请见这里

 

AJAX

AJAX可以使用jQuery或mmRequest, mmRequest体积更少,覆盖jQuery ajax模块的90%功能,并且在现代浏览器中使用了XMLHttpRequest2实现,性能更佳。

通过AJAX加载新数据到已存在的VM中

$.ajax({
     url: url,
     data: JSON.parse(JSON.stringify(vm.$model)), //去掉数据模型中的所有函数
     success: function(ajaxData) {
         //需要自己在这里定义一个函数,将缺少的属性补上,无用的数据去掉,
         //格式不正确的数据转换好 ajaxData最后必须为一个对象
         ajaxData = filterData(ajaxData)
         //先已有的数据,新的数据,全部拷贝到一个全新的空对象中,再赋值,防止影响原来的$model
         var newData = avalon.mix(true, {}, vm.$model, ajaxData)
         for (var i in newData) {
             if (vm.hasOwnProperty(i) && i !== "hasOwnProperty"){//安全更新数据
                 vm[i] = newData[i]
             }
         }
          
     }
 })

提交VM中的数据到后台,要小心死循环,详见这里

文件上传要用mmRequest的upload方法

扩展功能

avalon现在有三个扩展点,一是在avalon.fn上添加新的原型方法,这是用于处理DOM的,二是在avalon.bindingHandlers与 avalon.bindingExecutors上添加新的绑定(ms-xxx),三是在avalon.filters添加新的过滤器。

添加原型方法就不用多说,建议尽可能返回this,实现链式操作,this[0]为它包含的元素节点。

添加过滤器也很简,翻看源码看看lowercase如何实现就行了。

添加新绑定难一点,框架bindingHandlers要求对应的处理函数有两个参数,data与vmodels, data拥有如下几个属性:

  • element: 绑定了ms-xxx的元素,如<div ms-xxx-yyy='zzz'>innerHTML</div>,ms-xxx绑定所在的DIV元素。
  • value:是指mx-xxx绑定的这个特性节点的值,即上面的zzz。
  • param:是指mx-xxx绑定名以“-”分开几截,除了最前面的两部分外的东西,如这里的“yyy”。

vmodels是指,从DOM树最顶点到添加此绑定的元素所路过的ms-controller的值(它们都对应一个VM)。注意,ms-each, ms-with也产生VM。

bindingHandlers里的函数用于初始化绑定,它会对绑定属性做一些分解,放进parseExprProxy中,parseExprProxy会再调用parseExpr,将它转换为求值函数,放进行对应VM属性的subscribers数组内(操作方为registerSubscriber)。

bindingExecutors里的的函数为真正的视图刷新函数,每当VM发生改变后,都会被执行(操作方为notifySubscribers)。

可看这里

现在avalon拥有如此多绑定:

在IE6下调试avalon

由于IE6下没有console.log,如果又不想用VS等巨无霸IDE,可以自己定义以下方法

if(!window.console){
       window.console = {}
       console.log = function(str){
            avalon.ready(function() {
                var div = document.createElement("pre");
                div.className = "mass_sys_log";
                div.innerHTML = str + ""; //确保为字符串
                document.body.appendChild(div);
            });
       }
       
}

上线后,将.mass_sys_log{ display: none; }

如果是高级浏览器,avalon会在控制台上打印许多调试消息,如果不想看到它们,可以这样屏蔽它们:avalon.config({debug: false})

权限控制

将页面模块化,大量使用ms-include-src,没有权限就返回空页面,权限够了,但不是最高级,那它返回的模板文件也不一样/p>

posted on 2021-12-08 01:08  csjoz11  阅读(382)  评论(0编辑  收藏  举报