迷你MVVM框架 avalonjs 入门教程
- 关于AvalonJs
- 开始的例子
- 扫描
- 视图模型
- 数据模型
- 绑定属性与动态模板
- 作用域绑定(ms-controller, ms-important)
- 模板绑定(ms-include)
- 数据填充(ms-text, ms-html)
- 类名切换(ms-class, ms-hover, ms-active)
- 事件绑定(ms-on,……)
- 显示绑定(ms-visible)
- 插入绑定(ms-if)
- 双工绑定(ms-duplex)
- 样式绑定(ms-css)
- 数据绑定(ms-data)
- 布尔属性绑定(ms-checked, ms-selected, ms-readonly, ms-disabled, ms-enabled)
- 字符串属性绑定(ms-title, ms-src, ms-href……)
- 属性绑定(ms-attr)
- 循环绑定(ms-repeat)
- 数组循环绑定(ms-each废弃)
- 对象循环绑定(ms-with废弃)
- UI绑定(ms-widget)
- 模块间通信及属性监控 $watch,$fire, $unwatch
- 过滤器
- AMD加载器
- 路由系统
- AJAX
- 功能扩展
- 在IE6下调试avalon
- 权限控制
关于AvalonJS
avalon是一个迷你的MVVM框架,虽然从发布到现在,它臌胀了不少,但它现在还是比knockout小许多。avalon开发过程一直遵循三个原则:1,复杂即错误,2,数据结构优于算法,3,出奇制胜。这三大原则保证avalon具有良好的维护性,扩展性,与众不同。
简单说一下其他三大MVVM的实现思路:
- knockout:最早冒出来的JS MVVM库,通过转换VM中所有要监听的东西为函数,然后执行它们,得到某一时刻中,一共有多少函数被执行,将它们放到栈中,最底的就是最先被执行的,它上面的就是此函数所依赖的函数,从而得到依赖关系。 然后设计一个观察者模式,从上面的依赖检测中,将依赖函数作为被依赖者(最先执行的那个的)的订阅者,以后我们对被依赖者进行赋值时,就会通先订阅者更新自身,从而形成一个双向绑定链。 并且,knockout会将视图中的绑定属性进行转换,分解出求值函数与视图刷新函数,视图刷新函数依赖于求值函数,而求值函数亦依赖于我们VM中的某些属性(这时,它们都转换为函数),在第一次扫描时,它们会加入对应属性的订阅者列队中, 从而VM中的某个属性改变,就会自动刷新视图。
评价:实现非常巧妙,是avalon0.1-0.3的重要学习对象,但将属性变成一个函数,让人用点不习惯,许多用法都有点笨笨的。 虽然是一个轻盈的库,但扩展性不强,里面的实现异常复杂,导致能参与源码的人太少。 - emberjs: 一个大而全的框架,包罗万象。一开始是使用Object.defineProperty+观察者实现,但IE8的问题,让它不得不启用上帝setter, 上帝getter。没有自动收集依赖的机制,没有监控数组,计算属性需要自己指定依赖。VM可继承。 VM与视图的双向绑定依赖于其强大无比上万行的Handlebars 模板。听说是外国目前最好用的MV*框架。因为作者既是jQuery的核心成员,也是Rails的核心成员,虽然由于兼容问题没实现自动收集依赖,但框架的其他方面做得非常易上手,人性化。
评价:太大了,优缺点同python的Django框架。 - angular: google组织开发的框架,体现其算法至上的时候到了。里面一共有两个parser, 一个是ngSanitize/sanitize.js下的HTML parser, 一个是ng/parse.js(它要配合compile.js使用)的JS parser。第一个parser负责绑定抽取,第二个负责从Ctrl函数,工厂函数,服务函数及$watch回调中分解出无数setter, getter, 确认它们的依赖关系,放进观察者模式中。它的观察者无比强大,由于它的VM能继承,于是通过继承链实现四通发达的消息广播。它还实现了一个基于LRU的缓存系统,因为google最喜欢以空间换时间了,另一方面说明它要缓存的东西太多了,非常吃内存。 公司内部用angular实现的grid,200行在PC中就拖不动了。它还用到许多时髦的东东,如HTML5 history API, 迷你版Q Promise。内部是极其复杂。 不过最大的问题是,它是基于parser,静态编译,这意思着什么呢?不抗压缩!为此,它引进了IOC,官网上给出的简单例子其实在项目完全不可用,我们需要使用另一种更复杂的写法,方便编泽器从它们得到不被压缩的部分, 让它在压缩情况也能正常运行。由于基于编译,许多行为都不是即时的,可预见的。用户写的那些控制器函数,都是为编译做准备。由于基于编译,它不得不要求我们对具有兼容问题的一些全局函数,方法进行屏蔽,用它的给出的服务替代它们,如 window对应$window, document对应$document, location对应$location, setTimout对应$timeout……如果不遵循这规则,它可能运行不了,你需要手动使用$digest手动触发。
评价:乍一看入门非常容易,其实相反。条条框框太多,适合那些一直与JAVA搏斗的人士。对于那些接受过jQuery洗礼的人来说,用着用着会造反的……
现在的avalon是我在完全消化了knockout发展起来的,准确来说,是0.4版,通过Object.defineProperties与VBScript实现了与普通对象看起来没什么两样的VM,VM里面充满了访问器属性,而访问器属性肯定对应一个setter,一个getter, 我们就在setter, getter中走knockout的老路,实现自动收集依赖,然后放进一个简单的观察者模式中,从而实现双向绑定。将绑定属性分解为求值函数与视图刷新函数,早前,avalon也与knockout一样使用一个简单的parser,然后通过with实现, 0.82一个新的parser 上马,同样的迷你,但生成的求值函数,更方便依赖收集,并且没有with语句,性能更佳。angular也不是一无是处,我也从它那里抄来了{{}}插值表达式,过滤器机制,控制器绑定什么的。
avalon在内部使用了许多巧妙的设计,因此能涵盖angular绝对大多数功能,但体积却非常少。此外,在性能上,现在除了chrome外,它都比knockout快,angular则是最慢的。 在移动端上,avalon这个优势会被大大放大化的。
关于avalon的几点:
- 兼容IE6
- 配套着AJAX,动画,路由器,加载器,拖放等模块与功能
- avalon会自动同步视图,不用操作DOM,因此写出的业务代码比jQuery更少,从而维护性更好
迷你MVVM框架在github的仓库https://github.com/RubyLouvre/avalon, 如果你要兼容IE6,那么下其中的avalon.js, 如果你只打算兼容IE10与标准浏览器,那么下avalon.mobile.js。
官网地址http://rubylouvre.github.io/mvvm/
开始的例子
我们从一个完整的例子开始认识 avalon :
|
< 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 > |
avalon.define("box", function(vm) { |
vm.w = 100; |
vm.h = 100; |
vm.click = function() { |
vm.w = parseFloat(vm.w) + 10; |
vm.h = parseFloat(vm.h) + 10; |
} |
}) |
script > |
body > |
html > |
上面的代码中,我们可以看到在JS中,没有任何一行操作DOM的代码,也没有选择器,非常干净。在HTML中, 我们发现就是多了一些以ms-开始的绑定属性与{{}}插值表达式,有的是用于渲染样式, 有的是用于绑定事件。在ms-duplexl中,我们会发现它会反过来操作VM,VM的改变也会影响视图的其他部分。
扫描
不过上面的代码并不完整,它能工作,是因为框架默认会在DOMReady时扫描DOM树,将视图中的绑定属性与{{}}插值表达式抽取出来,转换为求值函数与视图刷新函数。
上面的JS代码相当于:
avalon.ready( function () { |
avalon.define( "box" , function (vm) { |
vm.w = 100; |
vm.h = 100; |
vm.click = function () { |
vm.w = parseInt(vm.w) + 10; |
vm.h = parseInt(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) |
} |
视图模型
我们是通过avalon.define函数返回一个视图对象VM,并且avalon.define(vmName, function(vm){})中的vm并不等于VM,工厂函数中的vm是用于转换为VM的。生成的VM比用户指定的属性还多了许多属性。
默认的,除了函数外,其他东西都转换为监控属性,计算属性与监控数组。如果不想让它转换,可以让此属性以 $开头,框架就不会转换它们。
如果实在不方便改名,又不想被转换,比如是一个jQuery对象或一个DOM节点,如果转换,肯定拖死框架,我们可以放到vm.$skipArray = [propName1, propName2] ($skipArray是一个字符串数组,不能放其他类型的东西进去)中去,这样也忽略转换。视图里面,我们可以使用ms-controller, ms-important指定一个VM的作用域。此外,在ms-each, ms-with中,它们会创建一个临时的VM,用于放置$key, $val, $index, $last, $first, $remove等变量或方法。
另外,avalon不允许在VM定义之后,再追加新属性与方法,比如下面的方式是错误的:
var vm = avalon.define( "test" , function (vm) { |
vm.test1 = '点击测试按钮没反应 绑定失败' ; |
}); |
vm.one = function () { |
vm.test1 = '绑定成功' ; |
}; |
//这里有两个错误, |
//1在命名上没有区分avalon.define的返回值与它回调中的参数, |
//2one方法的定义位置不对(这是考虑到兼容IE6-8,要求所有浏览器保持行为一致) |
此外,不要在avalon.define方法里面执行函数或方法,因此框架会对define执行了两次,第1次用于取得用户对vm对象(factory的传参)的设置,第2次用于重置里面的vm对象为真正的VM。看源码:
avalon.define = function (name, factory) { |
if ( typeof name !== "string" ) { |
avalon.error( "必须指定ID" ) |
} |
if ( typeof factory !== "function" ) { |
avalon.error( "factory必须是函数" ) |
} |
var scope = { |
$watch: noop |
} |
factory(scope) //得到所有定义 |
var model = modelFactory(scope) //偷天换日,将scope换为model |
stopRepeatAssign = true |
factory(model) |
stopRepeatAssign = false |
model.$id = name |
return VMODELS[name] = model |
} |
因此下面的写法会执行两次alert
function check(){ alert( "!!!!!!!!!!!" )} |
var model = avalon.define( "xxx" , function (vm){ |
vm.bool = true ; |
check() //这个方法不应该写在这里,请放在avalon.define外面 |
vm.array = [1,2,3] |
}) |
如果VM中的某函数是作为事件回调而存在,如ms-click=aaa, 如果想兼容IE6,aaa的vm最好替换为avalon.define返回的变量,确保能正确运行。
var model = avalon.define( "xxx" , function (vm){ |
vm.percent = 10 |
vm.aaa = function (){ |
model.percent ++; |
} |
}) |
所有定义好的VM都会储放在avalon.vmodels中!
我们再看看如何更新VM中的属性(重点):
< script > |
var model = avalon.define("update", function(vm) { |
vm.aaa = "str" |
vm.bbb = false |
vm.ccc = 1223 |
vm.time = new Date |
vm.simpleArray = [1, 2, 3, 4] |
vm.objectArray = [{name: "a"}, {name: "b"}, {name: "c"}, {name: "d"}] |
vm.object = { |
o1: "k1", |
o2: "k2", |
o3: "k3" |
} |
vm.simpleArray = [1, 2, 3, 4] |
vm.objectArray = [{name: "a", value: "aa"}, {name: "b", value: "bb"}, {name: "c", value: "cc"}, {name: "d", value: "dd"}] |
vm.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 > |
这里还有个例子,大家认真看看。
数据模型
当我们要用AJAX与后端交互时,如果直接把VM传上去太大了,这时我们需要把它对应的纯数组的JS对象。在VM中有个叫$model的属性,这是一个对象,就是数据模型M了。当我们更改VM时,框架就会自动同步M
绑定属性与动态模板
在开始之前,我们看一下静态模板是怎么工作的:
我之前写了一个叫ejs的静态模板引擎:
< script type = "tmpl" id = "table_tmpl" > |
<&= title() &> |
< table border = 1 > |
<&- for(var i=0,tl = @trs.length,tr;i< tl ;i++){ -&> |
<&- tr = @trs[i]; -&> |
< tr > |
< td ><&= tr.name;; &> td > < td ><&= tr.age; &> td > < td ><&= tr.sex || "男" &> td > |
tr > |
<& } &> |
table > |
< 怎么可能不支持图片 &> |
< img src="<&= @href &>"> |
script > |
它是以一个script标签做容器,里面的整个叫模板。模板里面有许多以 <& 与 &>划分出来的区块,用于插入JS代码,以@开头的变量是对应于数据包中的某个属性。
几乎所有静态模板的实现原理都是一样的,将这个模板变成一个函数,然后里面分成静态部分与动态部分,静态部分就是上面的HTNMl部分,转换为一个个字符串,动态部分就是插入的JS代码, 它们基本上原封不动地成为函数体的逻辑。然后我们传入一个对象给这个函数,最后得到一个符合HTML格式的字符串,最后用它贴到页面上某个位置就行了。
静态模板有几个缺点,首先它容易混入大量的JS逻辑,对于菜鸟来说,他们特别喜欢在里面放入越来越多JS代码。这个在JSP年代,已经证明是bad practice。为此出现了logic-less的 mustache。 其次,它更新视图总是一大片一大片地处理,改动太大。最后,是由于第2点引发的问题,它对事件绑定等不友好,因为一更新,原来的节点都被消灭了,需要重新绑定。幸好,jQuery普及了事件代理,这问题才没有 暴露出来。
再次,字符串模块没有对样式的操作,流程的操作进行封装,没有计算属性,监控数组的东西,很容易诱导用户在页面上写大量业务逻辑,导致代码无法维护。
下面就是一个PHP+原生JS+JQ的例子:
再看动态模板,几乎所有MVVM框架都用动态模板(当然也有例外,如emberjs)。动态模板以整个DOM树为容器,它通过扫描方式进行第一次更新视图。 在静态模板,通过<& 与 &>划分的部分,转换为绑定属性与{{}}插值表达式(这是一种文本绑定,在avalon中,我们可以通过|html过滤器,转换html绑定) 这样就有效阻止用户在页面上写逻辑。虽然动态模板也支持ms-if, ms-each等表示逻辑关系的绑定,但它的值最复杂也只能是一个表达式。 在绑定属性中,属性名用于指定操作行为,如切换类名,控制显示,循环渲染,绑定事件,数据填充什么的,而属性值是决定这些操作是否执行,与渲染结果。 由于双向绑定的关系,它不像静态模板那样,每次都要自己将数据包放进函数,得到结果,然后innerHTML刷新某个区域。它是在用户为VM的某个属性进行重新赋值,将视图中对应的某个文本节点, 特性节点或元素节点的值进行重刷。因此不会影响事件绑定。
在avalon中,这些视图刷新函数都有个element属性,保持对应的元素节点,每次同步时,都会检测此元素节点是否在DOM树,不在DOM树就取消订阅此刷新函数,节约内存,防止无效操作。
因此,你们可以看区别了吧。绑定属性与插值表达式就是对应静态模板中的JS逻辑部分,由于只允许为表达式或单个属性值,复杂度被控制了,强制用户将它们转移到VM中。 VM作为一个数据源,对应静态模板的数据包,并且多了一个自动触发功能,进化成一个消息中心。
< p ms-controller = "test" ms-click = "click" >{{ a }} p > |
< script > |
avalon.define("test", function(vm) { |
vm.a = '123'; |
vm.click = function() { |
vm.a = new Date - 0 |
} |
}) |
script > |
作用域绑定(ms-controller, ms-important)
avalon提供ms-controller, ms-important来指定VM在视图的作用范围。其他ms-*绑定属性或{{}}插值表达式,必须置于它们的作用域范围才能生效。比如有两个VM,它们都有一个firstName属性,在DIV中,如果我们用 ms-controller="VM1", 那么对于DIV里面的{{firstName}}就会解析成VM1的firstName中的值。
注意,avalon每扫描一个元素时,会移除对应的绑定属性或同名类名。这个特性非常有用,假如我们在页面使用了大量{{}}插值表达式,网速慢时,就会被用户看到,认为是乱码了。为了预防这种情况, 我们可以在样式中定义.ms-controller, [ms-controller]{ display:none},那么当元素被扫描后,它们才会安全显示出来。
有关它们的详细用法,可见这里。
模板绑定(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("test", function(vm) { |
vm.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来指定回调的名字。
下面是它们的实现
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("test", function(vm) { |
vm.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-*
|
< html > |
< head > |
< meta charset = "UTF-8" > |
< title >有关事件回调传参 title > |
< script src = "avalon.js" type = "text/javascript" > script > |
< script > |
avalon.ready(function() { |
var a = avalon.define("simple", function(vm) { |
vm.firstName = "司徒" |
vm.lastName = "正美" |
vm.array = ["aaa", "bbb", "ccc"] |
vm.argsClick = function(e, a, b) { |
alert(a+ " "+b) |
} |
vm.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伪类的渲染结果,因此比较有用。
|
< 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('Test', function(vm) { |
vm.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没有某个属性的情况。比如下面的情况:
|
< 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。
|
< 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('Test', function(vm) { |
vm.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。具体如下:
- ms-duplex="prop"
-
当元素为text, password, textarea时,要求prop为一个字符串,当我们改动它的内容时,avalon就会将此元素的value值赋给prop(在默认情况下,是使用input事件进行绑定,即每改动一个字符,都会进行同步,大家也可以指定 data-duplex-event="change",改用change事件进行绑定)当元素为radio时,要求prop为一个布尔, 当我们改动它的内容时,avalon就会将此元素的checked值(布尔)赋给prop当元素为checkbox时,要求prop为一个数组, 当我们改动它的内容时,avalon就会将此元素的value值push进prop当元素为select时,要求prop为一个字符串或数组(视multiple的值), 当我们选中它的某一个项时,avalon就会将此option元素的value值或text值(没有value时)push进prop。
- ms-duplex-text="prop"
- 只能用于radio, 用于模拟text控件的行为, 要求prop为一个字符串,当我们选中某一个radio时,avalon就会将此元素的value值赋给prop 用于实现多选一
- ms-duplex-radio="prop"
- 只能用于checkbox, 用于模拟radio控件的行为, 要求prop为一个布尔,当我们选中某一个checkbox时,avalon就会将此元素的checked值(布尔)赋给prop 多用于实现GRID中的全选/全不选功能
- ms-duplex-bool="prop"
- 只能用于radio, 要求prop为一个布尔,并且元素的value为“true”或“false”,当我们选中某一个radio时,avalon就会将此元素的value转换为布尔,赋给对应的prop。
注意:ms-duplex与ms-checked不能在同时使用于一个元素节点上。
注意:如果表单元素同时绑定了ms-duplex=xxx与ms-click或ms-change,而事件回调要立即得到这个vm.xxx的值,input[type=radio]是存在问题,它不能立即得到当前值,而是之前的值,需要在回调里面加个setTimeout。
ms-duplex-text, ms-duplex-radio与ms-duplex的用法,大家可以通过这个页面进行学习。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var model = avalon.define( "box" , function (vm) { |
vm.arr = [ "1" , '2' , "3" , "4" ] |
vm.selected = [ "2" , "3" ] |
vm.checkAllbool = vm.arr.length === vm.selected.length |
vm.checkAll = function () { |
if ( this .checked) { |
vm.selected = vm.arr |
} else { |
vm.selected.clear() |
} |
} |
}) |
model.selected.$watch( "length" , function (n) { |
model.checkAllbool = n === model.arr.size() |
}) |
|
|
|
对于非radio, checkbox, select的控件,我们可以通过data-duplex-changed来指定一个回调,传参为元素的value值,this指向元素本身,要求必须有返回值。
|
< 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('duplex', function(vm) { |
vm.username = "司徒正美" |
vm.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!
|
< html > |
< head > |
< title >by 司徒正美 title > |
< meta http-equiv = "Content-Type" content = "text/html; charset=UTF-8" > |
< script src = "../avalon.js" > script > |
< script > |
avalon.define("test", function(vm) { |
vm.o = 0.5 |
//vm.bg = "#F3F\9\0" |
vm.bg = "#F3F" |
}) |
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-*属性。
布尔属性绑定
这主要涉及到表单元素几个非常重要的布尔属性,即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)
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, $itemName等属性。另一个会产生VM对象的绑定是ms-widget。
- el: 不一定叫这个名字,比如说ms-each-item,它就变成item了。默认为el。指向当前元素。
- $first: 判定是否为监控数组的第一个元素
- $last: 判定是否为监控数组的最后一个元素
- $index: 得到当前元素的索引值
- $outer: 得到外围循环的那个元素。
- $itemName: 保存el(或item, elem)这个名字。
- $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 > |
|
< 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("array", function(vm) { |
vm.array = ["1", "2", "3", "4"] |
}) |
setTimeout(function() { |
a.array.set(0, 7) |
}, 1000); |
var b = avalon.define("complex", function(vm) { |
vm.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 > |
|
< 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('page', function(vm) { |
vm.arr = ["a", "b", "c", "d", "e", "f", "g", "h"] |
vm.object = { |
"kkk": "vvv", "kkk2": "vvv2", "kkk3": "vvv3" |
} |
vm.aaa = { |
aaa2: "vvv2", |
aaa21: "vvv21", |
aaa22: "vvv22" |
} |
vm.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' |
} |
vm.order = function() { |
return ["name", "sortStatus", "sortName", "method", "align"] |
} |
vm.dbobjec = { |
aaa: { |
aaa2: "vvv2", |
aaa21: "vvv21", |
aaa22: "vvv22" |
}, |
bbb: { |
bbb2: "ccc2", |
bbb21: "ccc21", |
bbb22: "ccc22" |
} |
} |
vm.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回调。
|
< 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("xxx", function(vm) { |
vm.obj = { |
aaa: "xxx", |
bbb: "yyy", |
ccc: "zzz" |
} |
vm.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 |
}) |
|
然后页面这样使用它
|
< html > |
< head > |
< meta charset = "utf-8" /> |
< script src = "avalon.js" > script > |
head > |
< body > |
< script > |
require("avalon.testui", function() { |
avalon.define("test", function(vm) { |
vm.$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 > |
模块间通信及属性监控 $watch,$fire, $unwatch
avalon内置了一个强大的自定义事件系统,它在绑定在每一个VM上。每一个VM都拥有$watch, $unwatch, $fire这三个方法,及一个$events对象。$events是用于储存各种回调。先从单个VM说起,如果一个VM拥有aaa这个属性,如果我们在VM通过$watch对它监控,那么当aaa改变值时,它对应的回调就会被触发!
var vmodel = avalon.define( "test" , function (vm){ |
vm.aaa = 111 |
vm.$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( "test" , function (vm){ |
vm.aaa = 111 |
vm.$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( "test" , function (vm){ |
vm.aaa = 111 |
vm.bbb = 222 |
vm.$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功能。
|
< html > |
< head > |
< title >by 司徒正美 title > |
< meta http-equiv = "Content-Type" content = "text/html; charset=UTF-8" > |
< script src = "avalon.js" > script > |
< script > |
avalon.define("ancestor", function(vm) { |
vm.aaa = '1111111111' |
vm.$watch("aaa", function(v) { |
avalon.log(v) |
avalon.log("ancestor.aaa事件被触发了") |
}) |
vm.click = function() { |
avalon.log("向下广播") |
vm.$fire("capture!aaa", "capture") |
} |
}) |
avalon.define("parent", function(vm) { |
vm.text = "222222222" |
vm.aaa = '3333333333' |
vm.$watch("aaa", function(v) { |
avalon.log(v) |
avalon.log("parent.aaa事件被触发了") |
}) |
vm.click = function() { |
console.log("全局扩播") |
vm.$fire("all!aaa", "broadcast") |
} |
}) |
avalon.define("son", function(vm) { |
vm.$watch("aaa", function(v) { |
avalon.log(v) |
avalon.log("son.aaa事件被触发了") |
}) |
vm.click = function() { |
console.log("向上冒泡") |
vm.$fire("bubble!aaa", "bubble") |
} |
}) |
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绑定
- uppercase
- 大写化
- lowercase
- 小写化
- truncate
- 对长字符串进行截短,truncate(number, truncation), number默认为30,truncation为“...”
- camelize
- 驼峰化处理
- escape
- 对类似于HTML格式的字符串进行转义,把尖括号转换为> <
- 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规范,可以见 这里
具体约定如下:
- 每个模块标识的字符串组成只能是合法URL路径,因此只能是英文字母,数字,点号,斜扛,#号。
- 如果模块标识是 以"./"开头,则表示相对于它的父模块的目录中找。
- 如果模块标识是 以"../"开头,则表示相对于它的父模块的父目录中找。
- 如果模块标识不以点号或斜扛开始,则有以下三种情况
- 如果此模块标识在 $.config.alias存在对应值,换言之某一模块定义了一个别名,则用此模块的具体路径加载文件。
- 如果此模块标识 以http://、https://、file:/// 等协议开头的绝对路径,直接用它加载文件。
- 否则我们将在引入框架种子模块(avalon.js)的目录下寻找是否有同名JS文件,然后指向它。
- 对于JS模块,它可以省略后缀名,即“.js”可有可无;但对于CSS需要使用css!插件机制。
- 框架种子模块的目录保存于 $.config.base属性中。
- ready!是系统占位符,用于表示DOM树是否加载完毕,不会进行路径转换。
如果想禁止使用avalon自带的加载器,可以在第一次调用require方法之前,执行如下代码:
|
|
|
|
|
|
|
avalon.config({loader: false }) |
alert(require) |
avalon.define( "xxx" , function (vm){ |
vm.aaa = "司徒正美" |
}) |
|
|
|
|
|
|
与jquery更好的集成,比如一些旧系统,直接在页面引入jquery库与其大量jquery插件,改成动态加载方式成本非常大。怎么样才能与jquery和平共存,亦能让AMD加载发挥作呢?先引入jquery库, 然后将avalon.modules.jquery 加个预设值(exports: jquery用于shim机制, state: 2 表明它已经加载完毕)就行了。
例子加载单个模块。
// 由于lang.js与mass.js是位于同一目录下,可以省略./ |
require( "lang" , function (lang) { |
alert(lang.String.toUpperCase( "aa" )) |
}); |
加载多个模块。需要注意的是,涉及DOM操作时必须要待到DOM树建完才能进入,因此我们在这里指定了一个标识,叫"ready!", 它并不一个模块,用户自定义模块,也不要起名叫"ready!"。
require( "jquery,node,attr,ready!" , function ($) { |
alert($.fn.attr + "" ); |
alert($.fn.prop + "" ); |
}); |
加载多个模块,使用字符串数组形式的依赖列表。
require([ "jquery" , "css" , "ready!" ], function ($, css) { |
$( "#js_require_ex3" ).toggle(); |
}); |
加载CSS文件。
require([ "jquery" , "ready!" , "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,ready" , 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 = "http://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的元素,如innerHTML,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>
更多学习资料
利用avalon 实现一个简单的成绩单, 教你如何使用ms-each数组循环绑定与$watch回调
如果您觉得此文有帮助,可以打赏点钱给我支付宝[email protected] ,或扫描二维码
» 下一篇: 迷你MVVM框架 avalonjs 0.84发布
posted on 2013-07-10 10:52 司徒正美 阅读(31574) 评论(74) 编辑 收藏