最近一直在学习MVC构建富应用的WEB程序,自己一直对MVC的设计模式理解的不是十分透彻,终于在研读了github上Spine的源码之后,对构建Model层有了一点自己的理解。
本文仅为个人理解,如有问题,欢迎指正。
对象关系映射(Ojbect-relational mapper,简称 ORM)是在除 JavaScript 以外的编程语言中常见的一种数据结构。然而在 JavaScript 应用中,对象关系映射也是一种非常有用的技术,它可以用来做数据管理及用作模型。比如使用 ORM 你可以将模型和远程服务捆绑在一起,任何模型实例的改变都会在后台发起一个 Ajax 请求到服务器端。或者你可以将模型实例和 HTML 元素绑定在一起,任何对实例的更改都会在界面中反映出来。本质上讲,ORM 是一个包装了一些数据的对象层。以往 ORM 常用于抽象 SQL 数据库,但在这里 ORM 只是用于抽象 JavaScript 数据类型。这个额外的层有一个好处,我们可以通过给它添加自定义的函数和属性来增强基础数据的功能。比如添加数据的合法性验证、监听、数据持久化及服务器端的回调处理等,这样会增加代码的重用率。
在我个人理解,其实就是在程序中创建一个虚拟数据库,与我们现实数据库通过映射关系对应。
$1. Model的建立
Model对象是为创建新模型与实例存在的。此处采用了javascript原型继承的方式。因为我个人大学一直是学习嵌入式开发,对面向对象的编程思想基本从未涉及。javascript原型继承的概念在此处不再赘述。以后上传javascript面向对象编程的读书笔记时会提到这点。
var Model = (function(){ if(typeof Object.create !== "function"){ Object.create = function(o){ function F(){}; F.prototype = o; return new F(); } } return { inherited: function(){}, created: function(){}, records: {}, create: function(){ var obj = Object.create(this); obj.parent = this; obj.prototype = obj.fn = Object.create(this.prototype); obj.created(); this.inherited(obj); return obj; }, init: function(){ var instance = Object.create(this.prototype); instance.parent = this; instance.init.apply(instance, arguments); return instance; }, extend: function(o){ var extended = o.extended; jQuery.extend(this, o); if(extended) extended(this); }, include: function(o){ var included = o.included; jQuery.extend(this.prototype, o); } } })(); Model.prototype = { init: function(attrs){ if(attrs){ for(var name in attrs){ this[name] = attrs[name]; } } } }
通过代码我们可以发现,我们构建了Model对象,我们使用的是基于原型的继承,没有用到构造函数和new关键字。
Object.create() 只有一个参数即原型对象,它返回一个新对象,这个新对象的原型就是传入的参数。换句话说,传入一个对象,返回一个继承了这个对象的新对象。当然我们也同时模拟出了这个函数。
这段代码其实在理解了Object.create()的基础上还是比较清晰地。Model.create()函数返回一个新对象,他继承自Model对象,我们使用他创建模型。Model.init()函数返回一个新对象,继承自Model.prototype。我们可以看作是Model对象的一个实例。
var Asset = Model.create(); var User = Model.create(); var user = User.init();
$2. 给Model添加属性
我们当然希望在model建立之初就可以对他的属性方法定义完全,但一般再优秀的架构师也不可能直接设计出完美的系统。所以当我们需要扩展我们的model时,我们肯定不希望我们还是在使用
model.check = function(){}, model.prototype.push = function(){}, 为了代码的优雅和体现函数复用,我们在上文extend与include两种方法。javascript中当一个对象在自己身上找不到某种特定方法时默认向上寻找,这个向上指原型指向,所以我们需要区分属性到底挂载在哪里,我们肯定不希望我们调用方法时出现undefined。extend用来扩展model的属性,include用来扩展model.prototype的属性。
所以当我们需要给某个实例来挂载方法时,我们肯定也希望其他实例也可以使用,我们需要使用include。代码中我们定义了parent即为他的父级对象。其实更好的方法还是去设定一下parent.name来指向具体的指向,更直白与简洁。
// 添加对象属性 Model.extend({ find: function(){} }); // 添加实例属性 Model.include({ init: function(atts) { /* ... */ }, load: function(attributes){ /* ... */ } });
$.3 持久化记录
我们需要一种保持记录持久化的方法,即将引用保存至新创建的实例中以便任何时候都能访问它。
我们通过在 Model 中使用 records 对象来实现。当我们保存一个实例的时候,就将它添加进这个对象中;当删除实例时,和将它从对象中删除。更新一个已存在的实例只需更新对象引用即可。
Model.include({ newRecord: true, create: function(){ this.newRecord = false; this.parent.records[this.id] = this; }, destroy: function(){ delete this.parent.records[this.id]; }, update: function(){ this.parent.records[this.id] = this; }, save: function(){ this.newRecord ? this.create() : this.update(); } }) Model.extend({ find: function(id){ return this.records[id] || console.log("Unknow record"); } })
我们定义了一个保存函数save(),这样就不用每次都检查这个实例是否已经保存过或是否需要新创建实例了。至此我们的基本数据模型就实现了。
var Asset = Model.create(); var asset = Asset.init({name: "tom", age: 20}); console.log(asset); asset.init({name: "mary",age: 30}); console.log(asset); asset.save(); console.log(asset); asset.destroy_on_prop(); console.log(asset);
此时可以断点添加监听调试,查看每一个函数执行之后asset的变化。
$.4 添加id查询与寻址引用
我们肯定希望在通过一个唯一标识在我们的数据模型中寻找具体的实例。此时,每次我们保存一条记录都必须手动指定一个 ID。这实在是糟透,但幸运的是,我们可以加入自动化处理。首先,我们需要一个方法来自动生成 ID,可以使用全局统一标识(Globally Unique Identifier,简称 GUID)生成器来做这一步。虽然 JavaScript 中内置的 Math.random() 方法尽管产生的是伪随机数,但也足够用了。
Math.guid = function(){ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); return v.toString(16); }).toUpperCase(); }; Model.extend({ create: function(){ if ( !this.id ) this.id = Math.guid(); this.newRecord = false; this.parent.records[this.id] = this; } });
我们将guid()方法重新集成到数据模型中,并对create()进行了重写。这样任何新创建的记录都包含一个随机的 GUID 作为它们的 ID。
但是此时存在关于引用有一个bug,当保存或通过 find() 查找记录时,所返回的实例并没有复制一份,因此对任何属性的修改都会影响原始资源。这的确是一个问题,因为我们只想当调用 update() 方法的时候才会修改资源。我们需要修复这个问题,在执行 find() 操作的时候新创建一个对象,同样在创建或更新记录的时候需要复制对象。同理,model.records此时是被所有模型共享的对象,这样在合并所有记录时会出现问题。解决办法是在创建新模型时设置一个新的records对象。Model.create()是创建新对象的回调,因此我们可以设置任意描述这个模型的对象。
Asset.extend({ find: function(id){ var record = this.records[id]; if ( !record ) throw("Unknown record"); return record.dup(); } }); Asset.include({ create: function(){ this.newRecord = false; this.parent.records[this.id] = this.dup(); }, update: function(){ this.parent.records[this.id] = this.dup(); }, dup: function(){ return jQuery.extend(true, {}, this); } }); //reset records Model.extend({ created: function(){ this.records = {}; } });
最后,我们的初始的数据模型已经构建完毕。相信有人注意到我们extend()不止是用来扩展属性方法,同样可以用于重写方法。这对于我们之后的调试来说是非常方便的。技术简陋,自己也是于MVC设计中在一点点寻找答案。如果有问题还希望各位大神指正。