迷你MVVM框架 avalonjs 学习教程2、模块化、ViewModel、作用域
一个项目是由许多人分工写的,因此必须要合理地拆散,于是有了模块化。体现在工作上,PM通常它这为某某版块,某某频道,某某页面。某一个模块,必须是包含其固有的数据,样式,HTML与处理逻辑。在jQuery时代,奉行的是“无侵入式javascript”,页面虽然是拆成一块块,但最后是通过PHP等后端模板合并起来,并且把第一屏的数据直接灌进去,接着是无尽的选择某些元素进行处理,选择某些元素进行处理。javascript里面是满屏的CSS表达式,如果不一一对着HTML页面,这是无法阅读的。换言之,jQuery很容易产生readyOnly的代码。
avalon是引入分层构架,视图就是视图,数据就是数据,JS里面是操作数据,不会再操作视图,泾渭分明。视图,换言之就是最初做好的那些HTML片段,只需要在里面添加上ms-controller指令(或叫绑定属性),指定其将要作用的ViewModel的ID,然后在它里面添加其他绑定就行了。数据,特指是ViewModel,avalon是通过define方法定义,目的是实现“操作数据即操作DOM”,从此我们再也用不上什么操作DOM的API,javascript代码量立即减少了一半以上,条理更清晰,更易维护。
ViewModel的定义是一个重头戏。在入门教程里,是这样定义的:
var model = avalon.define("test", function(vm) {
vm.firstName = "司徒"
vm.lastName = "正美"
vm.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;
}
}
vm.arr = ["aaa", 'bbb', "ccc", "ddd"]
vm.selected = ["bbb", "ccc"]
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()
})
有两个参数,第一个定义ID,第二个是定义ViewModel本身的数据,它有什么监听属性啊,计算属性啊,一些特殊的指令啊,$watch回调啊用户需要区分vm与model的区别,有什么需要注意的地方(这些在入门教程都有介绍)。在1.3.3中,添加了现在这种新的定义方式,只要传入一个对象:
var model = avalon.define({
$id: "test",
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;
}
},
arr: ["aaa", 'bbb', "ccc", "ddd"],
selected: ["bbb", "ccc"],
checkAllbool: false,
checkAll: function() {
if (this.checked) {
model.selected = model.arr
} else {
model.selected.clear()
}
}
})
model.checkAllbool = model.arr.length === model.selected.length
model.selected.$watch("length", function(n) {
model.checkAllbool = n === model.arr.size()
})
- 监控属性:就是改变了它会同步视图的属性;
- 非监控属性:就是改变了它不会同步视图的属性,通常是以$开头,或放在$skipArray数组的属性(angular到1.3才引入单向数据绑定);
- 计算属性:就是一个定义set, get方法的对象,是一种高级的监控属性;
- 监控数组:就是一个数组,如果它没有以$开头,或名字没有放到$skipArray数组里,框架就会自动转换它为监控数组,当用户调用它的方法时,就会同步视图通常它是与ms-repeat、ms-each指令配合使用。
当我们将一个对象传进avalon.define方法,它将返回一个全新的对象,它添加了许多$方法与属性,并且原来的属性都变得非常奇怪,在控制台下可以看到它们都对着一个set方法一个get方法。ecma262 v5称之为访问器属性(named accessor properties)。当然不同的人有不同的译法,大家想详细了解此属性的特性,可以阅读以下链接,这是avalon能让你修改属性就能同步视图的关键!
注意,我们所有定义的VM都存放在avalon.vmodels对象上。打开我们上一节写的项目,在firebug下输入avalon.vmodels可以查看到:
那么为了应用这些ViewModel,我们就需要用到ms-controller、 ms-important、ms-skip这三个指令。ms-controller在页面上表现为一个特殊的属性,其属性值为ViewModel的$id,表示将在此元素或其子孙元素上圈定它的作用域范围,但如果这些HTML存在它没有的属性,它可以向上查找上一级的ViewModel的属性。换言之,ms-controller可以互相套嵌的。 ms-important的用法与ms-controller差不多,但它不会向上查找。ms-skip注明这块区域不应用任何的ViewModel的属性,它里面的任何指令(绑定属性)都会失效。因为{{}}也算一种指令,而任何指令在被扫描后都会被移除,如果我们想保留某个区域的{{}},就需要用到ms-skip。有关ms-controller, ms-important的详细用法可见这里。
上面的ViewModel再配合一些HTML代码,就是实现一些用jQuery非常费劲才能实现的功能
<div ms-controller="test">
<p>First name: <input ms-duplex="firstName" /></p>
<p>Last name: <input ms-duplex="lastName" /></p>
<p>Hello, <input ms-duplex="fullName"></p>
<div>{{firstName +" | "+ lastName }}</div>
<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>
大家可以在这里看到实际运行效果。
再细说一下ViewModel(我们通常也简称为VM)的一些属性。
$id: VM的ID,方便在avalon.vmodels里查找到它,或用在ms-controller、ms-important上。
$events:里面存放着各种回调,它们是通过$watch方法添加的。
$watch:这是一个方法,有两个参数,第一个是VM中的某一个属性名,只能这个VM的直接子属性名,第二个是回调函数,当此属性发生改变时,就会执行此回调。回调里会依次传入它的新老属性值。
$unwatch:移除某个属性的回调。
$fire:手动触发此回调。
$accessors:放置与监听属性相连动的视图刷新函数,当我们改变某一属性时,框架就会在这里找到对应的视图刷新函数,传入当前值,实现对视图的同步。
$123323213:它的格式是$加上一串数字,它是用于放置监控数组的视图刷新函数,当我们调用监控数组的方法时,框架就此根据当前数组的个数与排列顺序,重新渲染对应的区域。它与$accessors一样,不开放给用户调用的。
$model:就是ViewModel的净化版,没有$XXX属性,访问器属性全部还原为普通属性,专门用于提交到后台用。当然我们提交后台,还需要用JSON.parse(JSON.stringify(VM.$model))处理一下,将里面的函数干掉。
现在说的还是基本用法,$watch、$unwatch、 $fire其实远远比你想象的强大,大家感兴趣的话,可以到这里了解其高级用法。
好了,我们把上面的代码放进上一节,修改aaa.js, aaa.html,感受一下一个复杂的ViewModel的应用吧。
有了ViewModel后,我们的代码就显得非常有内聚力,自己知道要作用于视图的哪一块区域,并且不用自己操心如此修改DOM,变成单纯的数据操作。
而数据操作是需要在页面定义一些指令(我们称之为绑定属性与插值表达式)。现在最简单的有两个,{{ prop }}是直接将属性输出到页面,如果它存在尖括号,会原样输出,不会转换为HTML标签。{{ prop | html}}则相反,比如这个属性的值为”xxxxerer”,那么里面就真会转为一个b标签。其实{{prop}},{{prop|html}}还有另一种写法, ms-text=”prop”, ms-html=”prop”。有关这些绑定的属性详细用法,我们下一节讲述。
本章节的代码可以从这里下载。