原文: Data Modeling for MVC Applications
模型-视图-控制器(MVC)是程序开发的主流范式。我们来看看Dojo为开发MVC的应用提供了些什么。我们将学习如何利用Dojo的stores对象与为模型提供的状态对象,以及如何在模型级别之上建立模型化的视图与控制器。
MVC应用的数据模型
MVC是应用开发的主流范式。MVC为使代码有组织性,易管理性对关注点做了分层。Dojo是重度基于MVC法则的,并为构建MVC架构的应用提供了强大的帮助。一个设计良好的MVC应用的基石就是数据模型。我们来看看如何利用Dojo的stores对象与状态对象(Stateful objects)创建一个健壮的模型,并且视图与控制器代码使用它。
模型
模型就是MVC中的M。数据模型代表应用程序使用与操作的核心信息。模型是应用的中心,视图与控制器为用户连接数据提供友好的途径。模型封装了数据的存储与校验。
Dojo的store对象在Dojo应用中实现了模型的角色。store接口为实现应用中数据与其他分离为设计。这样采用不同的存储媒介就不需要更改store接口了。Stores是可以扩展的,不只具有存储的能力。让我们来看看基本store的构造。我们将使用一个JsonRest store并将其返回的结果缓存起来:
1 require([ 2 "dojo/store/JsonRest", 3 "dojo/store/Memory", 4 "dojo/store/Cache", 5 "dojo/store/Observable" 6 ], function(JsonRest, Memory, Cache, Observable) { 7 masterStore = new JsonRest({ 8 target: "/Inventory/" 9 }); 10 cacheStore = new Memory({}); 11 inventoryStore = new Cache(masterStore, cacheStore); 12 });
现在inventoryStore代表基本数据模型。我可以使用get()方法取出数据,query方法来查询,put方法来修改。store通过处理服务器交互,封装了信息的存储。
视图可以连接到查询结果:
1 results = inventoryStore.query("some-query"); 2 viewResults(results); 3 4 // pass the results on to the view 5 function viewResults(results) { 6 var container = dom.byId("container"); 7 8 // result object provides a foreach method for interation 9 results.forEach(addRow); 10 11 function addRow(item) { 12 var row = domConstruct.create("div", { 13 innerHTML: item.name + " quantity: " + item.quantity 14 }, container); 15 } 16 }
viewResults方法为数据模型扮演了视图的角色。我们也可以利用dojo/string包中的substitute方法使用模版:
1 function addRow(item) { 2 var dom = domConstruct.create("div", { 3 innerHTML: string.substitute(tmpl, item); 4 }, container); 5 }
集成数据绑定
视图监视数据模型是MVC中重要的一个方面,为响应变更做准备。这样避免了控制器与视图不必要的连接。控制器应该更新模型,然后视图观察并对改变做出响应。我们可以使用dojo/store/Observable包裹器使模型可观察:
1 masterStore = new Observable(masterStore); 2 ... 3 inventoryStore = new Cache(masterStore, cacheStore);
这样视图就可以使用观察方法监视查询结果。
1 function viewResults(results) { 2 var container = dom.byId("container"); 3 var rows = []; 4 5 results.forEach(insertRow); 6 7 results.observe(function(item, removedIndex, insertedIndex) { 8 // this will be called any time a item is added, removed, and updated 9 if(removedIndex > -1) { 10 removeRow(removedIndex); 11 } 12 13 if(insertedIndex > -1) { 14 insertRow(item, insertedIndex); 15 } 16 }, true); // we can indicate to be notified of object updates as well 17 18 function insertRow(item, i) { 19 var row = domConstruct.create("div", { 20 innerHTML: item.value + " quantity: " + item.quantity 21 }); 22 rows.splice(i, 0, container.insertBefore(row, rows[i] || null)); 23 } 24 25 function removeRow(i) { 26 domConstruct.destroy(rows.splice(i, 1)[0]); 27 } 28 }
我们现在有了一个视图可以对于模型的变化及时作出响应,我们的控制器代码可以根据用户的交互响应利用store改变数据。控制器代码可以使用put(),add()和remove()方法影响改变。典型的控制器代码使用事件绑定,例如,我们可以在点击添加按钮时创建一个新的数据对象:
1 on(addButton, "click", function() { 2 inventoryStore.add({ 3 name: "Shoes", 4 category: "Clothing", 5 quantity: 30 6 }); 7 });
在视图中将会触发一个更新,我们无需直接与视图做交互。控制器代码单一的对用户动作作出响应,并控制模型。模型数据存储与视图渲染安全从控制器代码中分离。
丰富数据模型
我们之前的模型使用很简单,对于简单的数据存储都不需要添加什么逻辑操作(尽管服务端需要有额外的逻辑和检测)。我们可以在我们的引用中,在不影响其他组件情况下添加更多的功能。
检测
对于store我们添加的第一个扩展应该是想要加上检测。对于JsonRest store是很简单的,因为所有的更新都是通过put方法(add会调用put)。我们可以为inventoryStore在构造调用中添加一个put方法:
1 var oldPut = inventoryStore.put; 2 inventoryStore.put = function(object, options) { 3 if(object.quantity < 0) { 4 throw new Error("quantity must not be negative"); 5 } 6 7 //now call the original 8 oldPut.call(this, object, options); 9 };
现在更新会通过我们的检测逻辑做检查:
1 inventoryStore.put({ 2 name: "Donuts", 3 category: "Food", 4 quantity: -1 5 });
这将会抛出一个错误而拒绝改变数据。
层级
就像给数据模型添加逻辑一样,我们也给原生数据赋予意义。其中之一就是我们添加模型是层级展示。store对象定义了一个getChildren方法,我们就可以实现父子关系视图。有不同的方法我们可以存储这些关系。
存储的对象可以只有一个子对象的数组引用。对于小的列表这是一个很好的设计。相反对象可以追踪到他们父对象。后者是一个可伸缩的设计。
为了实现后者,我们可以简单地添加一个getChildren方法。在示例中我们的层级来自分类对象,每个有子对象。我们将创建一个getChildren方法,根据分类的name的属性获取其子对象列表,所以对于子对象有了一个子父关系:
1 inventoryStore.getChildren = function(parent, options) { 2 return this.query({ 3 category: parent.id 4 }, options); 5 };
层级视图可以调用getChildren来获取子对象列表而不需要知道数据的构造。如下:
1 require(["dojo/_base/Deferred"], function(Deferred) { 2 Deferred.when(inventoryStore.get("Food"), function(foodCategory) { 3 inventoryStore.getChildren(foodCategory).forEach(function(food) {}); 5 }); 6 });
我们可以获取一个对象的子对象,我们来看看如何改变一个对象的子对象集合。当使用inventoryStore时我们知道层级关系由分类属性已定义。如果我们想要把一个子项移到另一个分类下,我们可以简单地改变分类的属性就好了:
1 donut.category = "Junk Food"; 2 inventoryStore.put(donut);
一个关键的概念是Dojo的store为数据模型和其他组件之间提供了一致的接口。如果我们想要定义一个层级关系,组件在不知对象构造的情况下设置对象的父元素,我们可以是用put方法的options参数的parent属性:
1 inventoryStore.put = function(object, options) { 2 if(options.parent) { 3 object.category = options.parent; 4 } 5 // ... 6 };
现在我们可以改变分类的父元素:
1 inventoryStore.put(donut, {parent: "Junt Food"});
有序的Store
默认的,Store是无序的。然后如果Store有规律我们可以很容的实现一个有序的store。首页在查询确保返回的对象是有序的。并不需要对store做什么扩展。
有序store还提供了一个接口用于对象在有序store中移动。应用可能需要对象移上移下,置顶置尾。这在put方法的options参数的before属性已经有了:
1 inventoryStore.put = function(object, options) { 2 if(options.before) { 3 object.insertBefore = options.before.id; 4 } 5 };
服务端可以返回有序对象的inertBefore属性了。控制器代码中移动对象大体如下:
1 require(["dojo/on"], function(on) { 2 on(moveUpButton, ".move-up:click", function() { 3 inventoryStore.put(inventoryStore.get(this.itemId), { 4 before: inventoryStore.get(this.beforeId) 5 }); 6 }); 7 });
事务
事务是很多应用的关键部分,应用逻辑常常需要定义哪些操作需要自动合并。事务实现的一种方法就是收集事务的素有操作然后当事务提交时将他们作为一个请求发送。例如:
1 require(["dojo/_base/lang"], function(lang) { 2 lang.mixin(inventoryStore, { 3 transaction: function() { 4 // start a transaction, create a new array of operations 5 this.operations = []; 6 var store = this; 7 return { 8 commit: function() { 9 return xhr.post({ 10 url: "/Inventory/", 11 postData: JSON.stringify(store.operations) 12 }); 13 }, 14 abort: function() { 15 store.operations = []; 16 } 17 }; 18 }, 19 put: function(object, options) { 20 this.operations.push({action: "put", object: object}); 21 }, 22 remove: function(id) { 23 this.operations.push({action: "remove", id: id}); 24 } 25 });
26 });
我们创建一下自定义的操作来组成一个事务:
1 removeCategory: function(category) { 2 // atomically remove entire category an the items within the category 3 var transaction = this.transaction(); 4 var store = this; 5 this.getChildren(category).forEach(function(item) { 6 store.remove(item.id); 7 }, this).then(function() { 8 store.remove(category.id); 9 transaction.commit(); 10 }); 11 }
对象数据绑定:dojo/Stateful
Dojo在集合与数据模型之间做了清晰的分层。Dojo store提供集合层面的架构。我们来看看对象个体的模型。Dojo使用同样的概念,模型个体有统一的接口。我们可以使用dojo/Statefull接口影响对象。接口很简单,有三个方法:
- get(name) - 获取给定名称属性的值
- set(name, value) - 设置给定名称属性的值
- watch(name, listener) - 为给定属性的改变注册一个回调函数(第一个参数如何省略,会监听每个改变)
这些接口像store为视图那样给数据提供了同样的机会,他们可以渲染数据且及时反射数据的变更。我们来创建一个视图将一个对象绑定到一个Form表单上。首先,我们的HTML代码是这样的:
1 <form id="itemForm"> 2 Name: <input type="text" name="name" /> 3 Quantity: <input type="text" name="quantity" /> 4 </form>
然后绑定数据到HTML:
1 function viewInForm(object, form) { 2 // copy inital values into form inputs 3 for (var i in object) { 4 updateInput(i, null, object.get(i)); 5 } 6 7 // watch for an future changes in the object 8 object.watch(updateInput); 9 function updateInput(name, oldValue, newValue) { 10 var input = query("input[name=" + name + "]", form)[0]; 11 if (input) { 12 input.value = newValue; 13 } 14 } 15 }
表单关联store中的数据:
1 require([ 2 "dojo/Stateful", 3 "dojo/_base/Deferred" 4 ], function(Stateful, Deferred) { 5 item = new Stateful(item); 6 viewInForm(item, dom.byId("itemForm")); 7 });
控制器代码中修改对象,视图将立即响应:
1 item.set("quantity", 4);
一个表单在这种情况下,我也许想要添加一个onchange事件监听器,那样当输入框变化时就可以更新数据对象,以便做双向数据绑定(改变对象反射到表单中,改变表单反射到数据对象上)。Dojo在表单管理方面提供了很多高级的表单交互功能。
记住包裹对象可以也应该在变更提交时调用put方法保存数据。控制器代码如下:
1 on(saveButton, "click", function() { 2 inventoryStore.put(currentItem); // save the current state of the Stateful item 3 });
dstore: dojo/store的未来
新的dstore包是dojo/store的替代者,在版本1.8及其之后可用,是为Dojo 2计划的API。如果你是Dojo新手,我们建议你看看dstore。它也包含对集合与数据模化的支持。
总结
通过使用Dojo的store架构与状态接口,我们有了一个坚实的数据模型功能来构建MVC应用。视图和渲染数据模型并监视数据的变化。控制器可以在不耦合数据构造与不变动视图的情况下与数据以统一的方式交互,集合与实体接口被彻底的分离。所有这些可以帮你快速的构建分层良好的可管理的应用。