整合的第一步,得先把客户端的架子搭起来。
在这之前,还不了解Knockout.js的朋友建议先去knockoutjs.com把玩一下官方的tutorial。中文资料可以在网上搜,例如 http://yangshen998.iteye.com/blog/1310194 这个就看上去似乎不错。
前期准备
我们的产品本来就已经引入jquery 1.11,json2(为了IE7兼容)和underscore.js (http://underscorejs.org/) 这几个库,因此这里框架代码也会依赖这些库(如果只支持新式浏览器的话就不需要引入json2,浏览器已经自带API)。同时,为了方便把简单的Java对象转换为监控对象,需要引入ko.viewmodel库( http://coderenaissance.github.io/knockout.viewmodel/ )
首先,考虑到方便在日常使用中利用浏览器console的自动补全功能,以及隔离命名空间,我们先引入一个app命名空间,后续的框架代码与View Model都以此为根。
;(function($, win, undefined) {
if (!win.app) win.app = {}
/* 后续代码插入此处 */
})(jQuery, this)
考虑到旧式浏览器对console的支持不好,以及便于在生产环境中屏蔽日志,我们先统一console的API。以下代码插入到前面代码的注释处,下同。
;(function() {
var unified = win.console || {};
var dummy = {};
_.each([
"assert", "clear", "count", "debug", "dir", "dirxml", "error",
"group", "groupCollapsed", "groupEnd", "info", "log", "time", "timeEnd",
"trace", "warn", "table"
],
function(m) {
if (!unified[m]) unified[m] = _.noop;
dummy[m] = _.noop
});
win.app.enableConsole = function() {
win.app.consoleEnabled = true
win.app.console = unified
}
win.app.disableConsole = function() {
win.app.consoleEnabled = false
win.app.console = dummy
}
win.app.enableConsole()
})()
这样我们就保证了通过app.console来调用console命令时,即使浏览器不支持该接口,也不会抛出异常。并且可以通过app.enableConsole()和app.disableConsole()来开启或屏蔽输出到控制台。
接下来我们准备一些常用的工具方法。(题外话,我个人不太喜欢污染内置对象prototype的做法,因此倾向于选用Underscore.js而非Prototype,这里也选择使用工具方法而非修改内置对象的prototype)
win.app.utils = {
/**
* 判断字符串s是否以某子串结尾
*/
stringEndsWith: function(s, suffix) {
return s.indexOf(suffix, s.length - suffix.length) != -1
},
/**
* 去掉字符串前后的空格。(借用了《JavaScript框架设计》中介绍的实现)
*/
trimString: function(s) {
s = s.replace(/^\s+/, "")
for (var i = s.length - 1; i >= 0; i--) {
if (/\S/.test(str.charAt(i))) {
s = s.substring(0, i + 1)
break
}
}
return s
}
/**
* JSF生成的id通常带有冒号,与jQuery选择器冲突,此函数对id进行转义,以便用作jQuery选择器。
*/
idToSelector : function(id) {
return id.replace(/:/g, "\\:")
},
/**
* 生成一个在当前环境中唯一的数字序列ID。
* app.utils.id.next() 取下一个ID
* app.utils.id.peek() 取当前ID,保存计数器不变
*/
id: (function() {
var _id = 0;
return {
next: function() {
_id = _id + 1
return _id
},
peek: function() {
return _id
}
}
})()
}
定义View Model
Knockout.js对View Model的形式没有严格要求,只要是个对象就行。但出于统一代码风格与便于扩展的考虑,还是全部统一为先写构造器再从构造器创建实例的方法。这样在复杂场景中可以支持对同一类型的ViewModel创建多个实例,分别绑定不同区域。
为了便于查找与调试,规定把所有ViewModel构造器放在app.constructors中,把实例化出来的ViewModel对象放在app.vm中。为了强制推行这个规定,提供一个app.registerConstructor方法来创建并同时注册构造器。
;(function() {
/*TODO 定义bind方法*/
function bind(elem) {
}
function addVM(vmName, vm) {
/* 如果在app.vm中已存在同名实例,先对原实例进行清理 */
if (win.app.vm[vmName]) {
//清理
var oldVM = win.app.vm[vmName]
/* 调用模型上的__destroy__方法,可以在此方法中加入一些自定义清理动作 */
oldVM.__destory__ && oldVM.__destory__();
_.each(oldVM.__elements__, function(el) {
ko.cleanNode(el)
})
oldVM.__elements__ = null
}
/* 把模型名称保存到模型的__vmName__属性中,用于鉴别模型身份 */
vm.__vmName__ = vmName;
/* 模型的__elements__属性保存绑定到此模型上的根元素实例 */
vm.__elements__ = []
/* 把模型保存到app.vm中 */
win.app.vm[vmName] = vm;
}
_.extend(win.app, {
constructors: {},
vm: {},
registerConstructor: function(name, func, isStatic) {
/* 如果isStatic参数为true且已存在同名构造器,则保留原构造器不变 */
if (isStatic && this.constructors[name]) return;
/* 为构造器原型加入bind方法 */
func.prototype.bind = bind //TODO bind方法尚未定义
/* 从构造器创建的视图模型实例的__from__属性为Ctor-<构造器名称> */
func.prototype.__from__ = "Ctor-" + name
var self = this;
/* 这里保存的constructor事实上是一个工厂函数,调用之将返回一个新的ViewModel实例。
* 调用时可以对将要创建的实例命名,如果缺省,则使用构造器的名称作为实例名称,
* 这说明正在创建的实例是该构造器的唯一实例或主要实例。
* 第二个参数是一个可选的对象参数,此对象将被传入用户定义的构造器中,用于携带
* 一些在创建实例时的上下文参数。
*/
self.constructors[name] = function(vmName, options) {
/* 整理传入参数 */
if (_.isObject(vmName)) {
options = vmName
vmName = undefined
}
/* 在创建对象时如果未指定实例名称,则使用构造器名称作为实例名称 */
if (!vmName) vmName = name
/* 创建ViewModel实例,传入options参数 */
var vm = new func(options)
addVM(vmName, vm)
return vm;
}
return self.constructors[name];
},
//TODO
createVM: function() {}
});
})()
注意这里在注册构造器的同时会为其prototype加入一个bind方法,这样由其创建的ViewModel都具有此方法,我们可以用app.vm.MyViewModel.bind(<元素>)的方式来把ViewModel应用到DOM元素上去。bind方法的实现将在后面讨论。
这样,我们就可以使用以下的方式来定义一个视图模型构造器
app.registerConstructor("MyForm", function(opts) {
var self = this
self.firstName = ko.observable(opts.firstName || "")
self.lastName = ko.observable(opts.lastName || "")
self.fullName = ko.pureComputed(function() {
return app.utils.trimString(self.firstName() + " " + self.lastName())
})
})
创建视图模型实例
要从视图模型构造器中创建一个视图模型实例,可以直接调用构造方法:
app.constructors.MyForm() //创建名为MyForm的实例
app.constructors.MyForm("form1") //创建名为form1的实例
app.constructors.MyForm({firstName: "Tom"}) //创建名为MyForm的实例并传入初始化参数。注意这个新的实例会在app.vm数组中覆盖前面所创建的MyForm实例。
app.constructors.MyForm("form2", {lastName: "Parker"}) //创建名为form2的实例并传入参数
此时,在浏览器控制台中输入
app.vm.
就可以看到已经创建的三个视图模型实例的列表。
但这种方法还是打字太多,此外我们还想对一些简单场景支持从JS对象直接创建视图模型的方式,因此考虑提供一个app.createVM接口。
下面的createVM方法加入到前面代码的TODO部分
createVM: function() {
var constructor, vmName, options, vm, self = this
/* 调整参数 */
if (_.isObject(arguments[0])) {
vm = arguments[0]
vmName = arguments[1]
} else if (_.isString(arguments[1])) {
constructor = arguments[0]
vmName = arguments[1]
options = arguments[2]
} else if (_.isObject(arguments[1])) {
vmName = constructor = arguments[0]
options = arguments[1]
} else if (_.isUndefined(arguments[1])) {
if (_.isString(arguments[0])) {
vmName = constructor = arguments[0]
}
}
if (constructor) { //从构造器创建VM
if (self.constructors[constructor]) {
vm = self.constructors[constructor](vmName, options)
} else {
self.console.error("Constructor " + constructor + " not defined.")
}
} else if (vm) { //直接使用Json对象作为VM,适用于视图模型仅用于保存数据,没有复杂行为的简单场景
if (_.isUndefined(vmName)) {
vmName = "vm_" + win.app.utils.id.next()
}
/* 调用ko.viewmodel插件把简单Json转化为监控对象 */
vm = ko.viewmodel.fromModel(vm)
/* 新创建的监控对象需另行加入bind方法并添加到app.vm列表中 */
vm.bind = bind
addVM(vmName, vm)
} else {
self.console.error("Illegal arguments.")
return
}
return vm
}
这样,我们就可以使用以下的多种形式来创建一个视图模型实例了
app.createVM("MyForm") //从MyForm构造器创建同名视图模型
app.createVM("MyForm", "form1") //从MyForm构造器创建名为form1的视图模型
app.createVM("MyForm", {"lastName": "Parker"}) //从MyForm构造器创建同名视图模型,并传入初始化参数
app.createVM("MyForm", "form2", {"firstName": "Tom"}) //从MyForm构造器创建名为form2的视图模型,并传入初始化参数
app.createVM({"firstName": "Tom", "lastName": "Parker"}) //从简单对象创建视图模型,注意这个简单的视图模型并没有fullName这个计算属性。由于这里缺省了模型名称,会自动分配一个vm_<数字>形式的视图名称。
app.createVM({"firstName": "Tom", "lastName": "Parker"}, "simpleForm") //从简单对象创建视图模型并命名为simpleForm
当然,使用JS对象直接创建视图模型的方式,其实也可以写比较复杂的逻辑。例如
app.createVM({
"firstName": "Tom",
"lastName": "Parker",
"fullName": ko.pureComputed(function() {
return this.firstName() + " " + this.lastName() /* 注1 */
}, this), /* 注2 */
"clear": _.bind(function() {
this.firstName("")
this.lastName("")
}, this) /* 注3 */
}, "form3") //从非监控的JS对象直接创建名为form3的视图模型
注1: 创建的视图模型通过ko.viewmodel插件转换为监控对象,因此需要使用函数方式来访问属性。
注2: ko.pureComputed与ko.computed等接受函数的方法通常会提供第二个参数,用来绑定该函数内部的this引用。
注3: 普通的回调方法需要借助underscore.js的bind方法来绑定this引用。如果确保程序只运行于新式浏览器,可以直接使用原生的Function.bind方法
"clear": function() {
this.firstName("")
this.lastName("")
}.bind(this)
视图模型应用到DOM元素
把视图模型应用到DOM元素,最直接的方式是使用Knockout的applyBindings方法。
<form id="MyForm">
<div>
<label>First Name:
<input data-bind="textInput: firstName"/>
</label>
</div>
<div>
<label>Last Name:
<input data-bind="textInput: lastName"/>
</label>
</div>
<span data-bind="text: fullName"> </span>
</form>
<script type="text/javascript">
var vm = app.createVM("MyForm")
ko.applyBindings(vm, document.getElementById("#MyForm"))
</script>
但这种方式并不能完全满足我的需求。还记得前一节提到需要解决的问题之一就是如何从出问题的DOM节点反向查找出绑定的JS代码,而Knockout的原生绑定方案只要求从JS到DOM单向引用。为了强制加入从DOM到JS的反向引用,我们需要自定义一个绑定方法,此方法要求应用绑定的根元素上具有一个data-vm属性,其值为待绑定的视图模型名称。
此外,由于在JSF中经常对页面进行局部重新渲染,被绑定的根结点很有可能被移出DOM树。我们希望在绑定新结点时,对旧结点进行检查,如果它已被移出DOM树成为孤立结点,则打断其到视图模型的引用,避免出现内存泄漏。
现在,我们可以补全前面代码中的bind方法了。
function intervalBind(vm, elem) {
//绑定视图模型到DOM元素上
ko.applyBindings(vm, elem)
//清理视图模型中的绑定元素记录,只保留仍在DOM树中的结点
var parts = _.partition(vm.__elements__, function(e) {
if (document.contains) return document.contains(e)
if (document.body.contains) return document.body.contains(e)
while (elem && ko.utils.tagNameLower(elem) != "html") elem = elem.parent
return !!elem
})
vm.__elements__ = parts[0]
//清理已脱离DOM树的孤立结点
_.each(parts[1], function(e) {
//这里根据网上一些非官方资料进行一些清理动作,尚未证明必要和有效。
//后续如需要在DOM结点脱离DOM树后进行默认清理动作,可补充到这里
ko.cleanNode(e)
})
//记录绑定的元素
vm.__elements__.push(elem)
//调用视图模型上的__init__回调方法
try {
if (vm.__init__) vm.__init__(elem)
} catch (e) {
win.app.console.error(e)
}
}
function bind(elem) {
var selector = elem
elem = $(elem)
var vm = this //此方法最终会被加入到视图模型实例中,this即为视图实例
if (elem.size() > 0) {
elem.each(function(idx, el) { //如果是选择器,可能会选中多个元素,这里逐个绑定
var vmName = $(this).attr("data-vm")
if (vmName == vm.__vmName__) {
if (_.indexOf(vm.__elements__, el) < 0) {
intervalBind(vm, el)
} else {
win.app.console.log("Element has already been bound to the same VM.")
win.app.console.log(selector)
}
} else {
win.app.console.error("Element must have data-vm attribute refer to " + vm.__vmName__)
win.app.console.error(this)
}
})
} else {
win.app.console.error("Element " + selector + " not found")
}
}
现在,我们就可以使用视图模型上的bind方法进行元素绑定了。
app.vm.MyForm.bind("#MyForm")
app.createVM("MyForm", "form4").bind("#MyForm4") //创建视图模型实例同时进行绑定
前面的示例,就可以改为
<form id="MyForm" data-vm="MyForm">
...
</form>
<script type="text/javascript">
app.createVM("MyForm").bind("#MyForm")
</script>
(本节完)
分享到:
相关推荐
Knockout.JS是一个轻量级的JavaScript库,专门用于实现MVVM(Model-View-ViewModel)模式。MVVM模式在客户端开发中非常流行,因为它简化了数据绑定和UI更新的过程。Knockout.JS的核心功能包括数据绑定、依赖跟踪和...
Knockout.js是一个非常流行的JavaScript库,它遵循MVVM(模型-视图-视图模型)设计模式,主要被用来帮助开发者实现JavaScript应用程序中丰富的用户界面。在本文中,我们将详细介绍Knockout.js的核心概念以及如何在...
Knockout.JS是一款强大的JavaScript库,它提供了数据绑定和依赖跟踪功能,使得在浏览器端创建复杂的用户界面变得更加简单。而ASP.NET MVC是一个流行的服务器端框架,用于构建动态、数据驱动的Web应用。 首先,了解...
Knockout.JS 是一个JavaScript库,专门用于实现MVVM(Model-View-ViewModel)模式,简化了前端数据绑定和动态用户界面的创建。在ASP.NET MVC 4项目中,Knockout.JS可以无缝集成,帮助开发者实现在客户端实时更新视图...
这个插件是为了解决在使用Knockout.js进行数据绑定时,对JavaScript对象与视图模型之间映射的问题。Knockout.js是一个MVVM(Model-View-ViewModel)框架,它使得JavaScript可以轻松地实现数据双向绑定,简化了DOM...
2. **数据绑定**:Knockout.js提供了一种声明式的方式来关联视图和ViewModel,使得当ViewModel中的数据发生变化时,视图会自动更新,反之亦然。常见的数据绑定包括文本绑定、事件绑定、条件绑定等。 3. **服务器端...
《ASP.NET MVC 5 with Bootstrap and Knockout.js》是由O'Reilly出版社于2015年出版的一本专业书籍,主要面向的是希望掌握ASP.NET MVC 5开发技术,并结合Bootstrap和Knockout.js构建现代Web应用程序的开发者。...
### Knockout.js Succinctly:全面解析与应用实践 #### 一、Knockout.js简介与特点 **Knockout.js**是一款轻量级的客户端MVC(Model-View-Controller)框架,专为简化现代Web应用程序开发而设计。其核心功能之一是...
Beginning with a vital overview of Knockout.js, including the MVVM design pattern, you will create a simple but powerful application capable of integrating with ASP.NET MVC as well as gain a thorough ...
基于 BOOTSTRAP 和 KNOCKOUT.JS 的 ASP.NET MVC 开发实战。 利用动态服务端Web内容和响应Web设计共同构建的网站,在任何分辨率、桌面或移动设备下都可以进行良好的显示。通过本书的实践应用,你将可以学习对ASP.NET ...
With this practical book, you’ll learn how by combining the ASP.NET MVC server-side language, the Bootstrap front-end framework, and Knockout.js—the JavaScript implementation of the Model-View-...
简单的knockout.js/websockets/node.js实时游戏 这是一个非常简单的实时游戏,使用 node.js、knockout.js 和 socket.io 创建。 ##目标 尝试比其他玩家更快地解决简单的数学方程。 每场比赛都有时间限制(默认...
Knockout.JS is an emerging JavaScript presentation framework that promotes a solid solution design in combination with Jasmine, a well-known unit testing library. They both ensure rapid development ...
JavaScript捆绑与最小化 小结 第14章构建数据模型 Code—First模型 定义DbContext并初始化数据 视图模型 小结 第15章布局实现 共享布局 购物车摘要 分类菜单 小结 第16章图书列表 主页 特色图书 按...
KnockOut抠图插件,支持32、64位版系统(与photoshop位数无关) 目前测试使用win10(64位)、photoshop cs6 13.0(32位) 1.下载后放在photoshop安装目录中的Plug-ins中(解压后Plug-ins文件夹中应该会包含:KnockOut.8bf...
**Knockout.js与jQuery Mobile的整合:使用Shim** 在Web开发中,Knockout.js和jQuery Mobile是两个非常流行的库,分别用于数据绑定和移动应用的UI交互。它们结合使用可以创建出功能强大且用户友好的动态移动界面。...
Knockout.js能够很好地与其他JavaScript库和框架协同工作,例如可以轻松地将Bootstrap或其他UI框架的组件与Knockout.js的绑定相结合。 #### 更改button的状态 通过绑定`disabled`属性,可以基于数据条件动态地启用...