avalon是一个简单易用迷你的MVVM框架,它最先发布于2012.09.15,为解决同一业务逻辑存在各类视图呈现而开发出来的。 事实上,这问题其实也能够简单地利用通常的前端模板加jQuery 事件委托 搞定,但随着业务的膨胀, 代码就充满了各类选择器与事件回调,难以维护。所以完全的将业务与逻辑分离,就只能求助于架构。 最初想到的是MVC,尝试过backbone,但代码不降反升,很偶尔的机会,碰上微软的WPF, 优雅的MVVM架构当即吸引住我,我以为这就是我一直追求的解决之道。javascript
MVVM将全部前端代码完全分红两部分,视图的处理经过绑定实现(angular有个更炫酷的名词叫指令), 业务逻辑则集中在一个个叫VM的对象中处理。咱们只要操做VM的数据,它就天然而然地神奇地同步到视图。 显然全部神秘都有其内幕,C#是经过一种叫访问器属性的语句实现,那么JS也有没有对应的东西。 感谢上帝,IE8最先引入这东西(Object.defineProperty),惋惜有BUG,但带动了其余浏览器实现它,IE9+便能安全使用它。 对于老式IE,我找了很久,实在没有办法,使用VBScript实现了。css
Object.defineProperty或VBS的做用是将对象的某一个属性,转换一个setter与getter, 咱们只要劫持这两个方法,经过Pub/Sub模式就能偷偷操做视图。为了记念WPF的指引,我将此项目以WPF最初的开发代号avalon来命名。 它真的能让前端人员脱离DOM的苦海,来到数据的乐园中!html
绝对的优点就是下降了耦合, 让开发者从复杂的各类事件中挣脱出来。 举一个简单地例子, 同一个状态可能跟若干个事件的发生顺序与发生时的附加参数都有关系, 不用 MVC (包括 MVVM) 的状况下, 逻辑可能很是复杂并且脆弱。 而且一般须要在不一样的地方维护相关度很是高的一些逻辑, 稍有疏忽就会酿成 bug 不能自拔。使用这类框架能从根本上下降应用开发的逻辑难度, 而且让应用更稳健。前端
除此以外, 也免去了一些重复的体力劳动, 一个 {value} 就代替了一行 $(selector).text(value)。 一些个经常使用的 directive 也能快速实现一些本来可能须要较多代码才能实现的功能java
avalon如今有三个分支:avalon.js 兼容IE6,标准浏览器, 及主流山寨浏览器(QQ, 猎豹, 搜狗, 360, 傲游); avalon.modern.js 则只支持IE10等支持HTML5现代浏览器 ; avalon.mobile.js,添加了触屏事件与fastclick支持,用于移动端node
咱们从一个完整的例子开始认识 avalon :jquery
<!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代码专一业务逻辑自己, 写得更易维护的代码! git
不过上面的代码并不完整,它能工做,是由于框架默认会在DOMReady时扫描DOM树,将视图中的绑定属性与{{}}插值表达式抽取出来,转换为求值函数与视图刷新函数。github
上面的JS代码至关于:ajax
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 } })
接着咱们说一些重要的概念:
$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的绑定(或指令),拥有如下三种类型:
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"//这个绑定属性有三个参数,表示三种不一样的拦截操做
若是一个页面很是复杂,就须要划分模块,每一个模块交由不一样的ViewModel去处理。咱们就要用到ms-controller与ms-important来指定ViewModel了。
咱们看下面的例子:
<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>
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() })
能够看出ViewModel在DOM树的做用范围其实与CSS很类似,采起就近原则,若是当前ViewModel没有此字段 就找上一级ViewModel的同名字段,这个机制很是有利于团队协做。
若是从另外一个角度来看,因为这种随机组成的方式就能实现相似继承的方式,所以咱们就没必要在JS代码时构建复杂的继承体系。
类的继承体系是源自后端复杂业务的膨胀而诞生的。早在20世界80年代初期,也就是面向对象发展的初期,人们就很是看重继承这个概念。 继承关系蕴涵的意义是很是深远的。使用继承咱们能够基于差别编程,也就是说,对于一个知足咱们大部分需求的类,能够建立一个它的子类,重载它个别方法来实现咱们所要的功能。只子继承一个类, 就能够重类该类的代码!经过继承,咱们能够创建完整的软件结构分类,其中每个层均可以重用该层次以上的代码。这是一个美丽新世界。
但类继承的缺点也是很明显的,在下摘录一些:
面向对象语言与生俱来的问题就是它们与生俱来的这一整个隐性环境。你想要一根香蕉,但你获得的是一头手里握着香蕉的大猩猩,以及整个丛林。 -- Joe Armstrong
在适合使用复合模式的共有类中使用继承,会把这个类与它的超类永远地束缚在一块儿,从而人为地限制了子类的性能
类继承的缺点
- 超类改变,子类要跟着改变,违反了“开——闭”原则
- 不能动态改变方法实现,不能在运行时改变由父类继承来的实现
- 破坏原有封装,由于基类向子类暴露了实现细节
- 继承会致使类的爆炸
所以在选择是继承仍是组合的问题上,avalon倾向组合。组合的使用范例就是CSS,所以也有了ms-important的诞生。
而ms-important就至关于CSS的important语句,强制这个区域使用此ViewModel,再也不往上查找同名属性或方法!
另,为了不未经处理的原始模板内容在页面载入时在页面中一闪而过,咱们可使用如下样式(详见这里):
.ms-controller,.ms-important,[ms-controller],[ms-important]{ visibility: hidden; }
这是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>
若是单是把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请求。
这分两种:文本绑定与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'",要用两种引号强制让它的内部不是一个变量。
avalon提供了多种方式来绑定类名,有ms-class, ms-hover, ms-active, 具体可看这里
avalon经过ms-on-click或ms-click进行事件绑定,并在IE对事件对象进行修复,具体可看这里
avalon并无像jQuery设计一个近九百行的事件系统,连事件回调的执行顺序都进行修复(IE6-8,attachEvent添加的回调在执行时并无按先入先出的顺序执行),只是很薄的一层封装,所以性能很强。
<!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的点击事件
avalon经过ms-visible="bool"实现对某个元素显示隐藏控制,它用是style.display="none"进行隐藏。
这个功能是抄自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方法。
这功能抄自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-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-name="value", 用于为元素节点绑定HTML5 data-*属性。
这主要涉及到表单元素几个很是重要的布尔属性,即disabed, readyOnly, selected , checked, 分别使用ms-disabled, ms-enabled, ms-readonly, ms-checked, ms-selected。ms-disabled与ms-enabled是对立的,一个true为添加属性,另外一个true为移除属性。
这主要涉及到几个很是经常使用的字符串属性,即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-name="value",这个容许咱们在元素上绑定更多种类的属性,如className, tabIndex, name, colSpan什么的。
用法为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。
咱们还能够经过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-repeat几乎一致,建议用ms-repeat代替。
语法为 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更高的用法,如双重循环什么的,能够看这里
它的格式为ms-widget="uiName, id?, optsName?"
下面是一个完整的实例用于教导你如何定义使用一个UI。
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自带如下几个过滤器
decimals 可选,规定多少个小数位。 dec_point 可选,规定用做小数点的字符串(默认为 . )。 thousands_sep 可选,规定用做千位分隔符的字符串(默认为 , ),若是设置了该参数,那么全部其余参数都是必需的。
'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装备了AMD模范的加载器,这涉及到两个全局方法 require与define
require(deps, callback)
deps 必需。String|Array。依赖列表,能够是具体路径或模块标识,若是想用字符串表示多个模块,则请用“,”隔开它们。
callback 必需。Function。回调,当用户指定的依赖以及这些依赖的依赖树都加载执行完毕后,才会安全执行它。
若是想禁止使用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 代表它已经加载完毕)就好了。
若是你想用其余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文件里。
avalon与seajs, https://github.com/RubyLouvre/avalon/issues/313
咱们也能够在源码里面直接移除AMD加载器模块。
它须要依赖于另外一个独立的组件mmRouter,用法请见这里
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拥有以下几个属性:
vmodels是指,从DOM树最顶点到添加此绑定的元素所路过的ms-controller的值(它们都对应一个VM)。注意,ms-each, ms-with也产生VM。
bindingHandlers里的函数用于初始化绑定,它会对绑定属性作一些分解,放进parseExprProxy中,parseExprProxy会再调用parseExpr,将它转换为求值函数,放进行对应VM属性的subscribers数组内(操做方为registerSubscriber)。
bindingExecutors里的的函数为真正的视图刷新函数,每当VM发生改变后,都会被执行(操做方为notifySubscribers)。
如今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>
利用avalon 实现一个简单的成绩单, 教你如何使用ms-each数组循环绑定与$watch回调