文章目錄
- MVC應用的資料建模(基於Dojo)
- Model
- 資料繫結
- 資料模型進階
- 資料繫結: dojo/Stateful
- 總結
本文翻譯自: http://dojotoolkit.org/documentation/tutorials/1.7/data_modeling/
MVC,即模型(Model),視圖(View)和控制Controller),是當今應用開發的主導模式。這裡,我們要從Dojo的基礎功能出發,來看看Dojo是如何支援MVC應用的。通過這篇文章我們會瞭解到如何通過Dojo的object stores 和 Stateful objects(有狀態的對象)來構建MVC應用,以及如何基於這些模型實現我們的顯示層和控制層。
MVC應用的資料建模(基於Dojo)
MVC模式是當今應用開發的主導模式。該模式主要目的是組織嚴密的,易於管理的的代碼結構。Dojo控制項陳列庫自身的代碼結構就是高度基於MVC模式的,同時也能很好的支援那些基於MVC模式的應用。一個好的MVC應用的核心是它能擁有一個完美的資料模式。接下來我們會介紹一下如何通過Dojo的Object stores和stateful objects(有狀態物件)去構建一個包含view和controller的完美模式。
Model
Model層是MVC中的“M”,資料層指的是那些應用中需要訪問和處理的核心資料資訊,他是一個應用的中心,viewer和controller層則主要用來作為使用者和Model(資料)層之間互動的一個橋樑。Model層則封裝了儲存和驗證的過程。
Dojo的object store完美的擔當了Dojo應用中Model的這一個角色。store的介面就是按照分離出資料層的思想來設計的。不同的儲存媒介基於相同的store介面。Stores本身也支援擴充更為強大的功能。接下來我們來看看如何構建一個基本的store。我們將使用一個JsonRest的store,同時緩衝住我們取到的資料:
require(["dojo/store/JsonRest", "dojo/store/Memory", "dojo/store/Cache", "dojo/store/Observable"], function(JsonRest, Memory, Cache, Observable){ masterStore = new JsonRest({ target: "/Inventory/", idProperty: "name" }); cacheStore = new Memory({ idProperty: "name" }); inventoryStore = Cache(masterStore, cacheStore)
現在,我們的“inventoryStore”就成了我們的資料層,我們可以通過“get()”取資料,“query()”查詢資料,“put()”修改資料。store這裡封裝了對真實資料的預存程序,包括與伺服器(server)端的互動。
我們的視圖層(viewer)便可以開始查詢結果了:
results = inventoryStore.query("some-query");viewResults(results); // pass the results on to the viewfunction viewResults(results){ var container = dom.byId("container"); // results object provides a forEach method for iteration results.forEach(addRow); function addRow(item){ var row = domConstruct.create("div",{ innerHTML: item.name + " quantity: " + item.quantity }, container); }}
我們的viewResults在這裡就是充當了資料層的一個viewer。我們還能夠通過dojo/string的substitute方法實現簡單的模板化視圖。
function addRow(item){ var row = domConstruct.create("div",{ innerHTML: string.substitute(tmpl, item); }, container);}資料繫結
MVC裡面比較重要的一塊就是view層應該監聽資料層的改動,然後及時的反應在介面上。這種方式避免了用controller去監聽資料層的無畏的資源開銷。Controller只需要更新model層,viewer會自動將該改動反映到應用的介面上。我們能夠通過dojo/store/Observable來實現這一點。
masterStore = Observable(masterStore);...inventoryStore = Cache(masterStore, cacheStore);
現在我們的viewer能夠通過observe的方式來監聽查詢結果的變化了
function viewResults(results){ var container = dom.byId("container"); var rows = []; results.forEach(insertRow); results.observe(function(item, removedIndex, insertedIndex){ // this will be called any time a item is added, removed, and updates if(removedIndex > -1){ removeRow(removedIndex); } if(insertedIndex > -1){ insertRow(item, insertedIndex); } }, true); // we can indicate to be notified of object updates as well function insertRow(item, i){ var row = domConstruct.create("div", { innerHTML: item.name + " quantity: " + item.quantity }); rows.splice(i, 0, container.insertBefore(row, rows[i])); } function removeRow(i){ domConstruct.destroy(rows.splice(i, 1)[0]); }}
現在我們viewer已經能夠即時的反映model資料的變化了,與此同時,我們的controller相關代碼也能夠基於使用者的操作來對model資料作出相應的修改。Controller可以通過put(), add(), 和 remove()來修改資料。通常來講,Controller相關代碼主要用來處理事件,比如:當使用者點擊add按鈕時,我們便建立一個新的資料對象。
on(addButton, "click", function(){ inventoryStore.add({ name: "Shoes", category: "Clothing", quantity: 40 });});
該行為會直接更新我們的應用介面(viewer),我們無需再用額外的代碼去直接操作我們的介面了。這個時候我們的Controller就只需要基於使用者的操作來修改model資料了。此刻,Model資料層的儲存和view層的更新渲染就與我們的邏輯完全隔離開來了
資料模型進階
之前我們用到的store都非常簡單,沒有包含任何邏輯(可能服務端會包含一些邏輯和驗證)。我們其實可以在不影響其他模組的同時對store加入一些額外的功能。
驗證
驗證功能便可作為store的一個擴充,這個擴充對JsonRestStore來說非常簡單,因為所有的更新都會調用put()方法(add() 會調用 put()),我們只需要擴充一下put方法即可。
var oldPut = inventoryStore.put;inventoryStore.put = function(object, options){ if(object.quantity < 0){ throw new Error("quantity must not be negative"); } // now call the original oldPut.call(this, object, options);};
此時,驗證邏輯已經被加入了。
inventoryStore.put({ name: "Donuts", category: "Food", quantity: -1});
由於quantity的值小於0,所以此時該方法會拋出異常。
Hierarchy階層
如同我們給我們的資料模型加入邏輯功能一樣,我們也為中繼資料加入了一些特有的含義,其中之一就是階層。object store定義了getChildren()方法是我們能夠實現我們的父---子結構。存放這種父---子結構有很多種方式。
stored objects可以存放一個指向其所有子物件的數組引用。這種做法適用於小的,順序的列表資料。同樣,objects也可以存放一個指向其父物件的引用,這種做法伸縮性更強。
為了實現第二種資料結構,我們可以加入getChildren()方法。在如下樣本中,我們的構造的階層來源於我們的含有獨立子物件的執行個體,我們建立getChildren()方法用於找到所有category屬性值為該父物件名稱的對象集合,這些對象就是該父物件的所有子物件。這就是將父/子關係定義為子物件的一個屬性的解決方案。
inventoryStore.getChildren = function(parent, options){ return this.query({ category: parent.name }, options);};
現在,我們可以不用管資料的內部結構,而直接通過getChildren()擷取父物件的所有子節點,檢索子節點方式如下:
require(["dojo/_base/Deferred"], function(Deferred){ Deferred.when(inventoryStore.get("Food"), function(foodCategory){ // retrieved the food category object, now get it's children inventoryStore.getChildren(foodCategory).forEach(function(food){ // handle each item in the food category }); });
至此,我們能取得子節點的對象,接下來我們來看看如何修改他們。我們知道category定義了資料的階層,如果我們改變某個對象元素的階層,我們只需要修改category的屬性值即可。
donut.category = "Junk Food";inventoryStore.put(donut);
Dojo stores的一個核心思想就是提供統一的資料操作介面。如果我們想簡單的通過設定父物件的方式來定義對象的階層,我們可以在put()方法裡使用options參數集裡的parent屬性。
inventoryStore.put = function(object, options){ if(options.parent){ object.category = parent; } // ...};
現在我們可以設定父物件了。
inventoryStore.put(donut, {parent: "Junk Food"});有序的Store
預設情況下,一個store通常是一群無序對象的集合。儘管如此,我們還是可以實現store的有序排列,尤其是對象集合中已經存在預留的隱含的序列屬性時。實現有序store的第一個工作就是在調用query()方法後返回一個有序的store(若且唯若沒有其他排序設定的影響)。實現它往往不需要對store作擴充,你只需要返回相應順序的資料序列即可。
有序的Store通常還有一個需求,就是其元素能夠前後移動,甚至直接到最前或者最後等等。我們可以通過在put()方法的options參數中加入before屬性來實現。
inventoryStore.put = function(object, options){ if(options.before){ // we set the reference object's name in the object's "insertBefore" // so the server can put the object in the right order object.insertBefore = options.before.name; } // ...};
伺服器現在可以通過insertBefore屬性來做排序了。我們的controller層的代碼可以開始移動我們的對象了(我們使用事件代理並假設節點的itemId和beforeId已經在建立時設定了):
require(["dojo/on"], function(on){ on(moveUpButton, ".move-up:click", function(){ // |this| in event delegation is the node // matching the given selector inventoryStore.put(inventoryStore.get(this.itemId), { before: inventoryStore.get(this.beforeId) }); }); 事物
事物是應用程式裡面比較重要的一塊,通常用於把一系列應用的邏輯操作綁定到一起,一次性執行。他的作用之一就是集合一系列操作然後通過單一請求一次性提交,樣本如下:
require(["dojo/_base/lang"], function(lang){ lang.mixin(inventoryStore, { transaction: function(){ // start a transaction, create a new array of operations this.operations = []; var store = this; return { commit: function(){ // commit the transaction, sending all the operations in a single request return xhr.post({ url:"/Inventory/", // send all the operations in the body postData: JSON.stringify(store.operations) }); }, abort: function(){ store.operations = []; } }; }, put: function(object, options){ // ... any other logic ... // add it to the queue of operations this.operations.push({action:"put", object:object}); }, remove: function(id){ // add it to the queue of operations this.operations.push({action:"remove", id:id}); } });
基於上述代碼,我們可以利用事物來構建我們的自訂動作:
removeCategory: function(category){ // atomically remove entire category and the items within the category var transaction = this.transaction(); var store = this; this.getChildren(category).forEach(function(item){ // remove each child store.remove(item.name); }, this).then(function(){ // now remove the category store.remove(category.name); // all done, commit the changes transaction.commit(); });}資料繫結: dojo/Stateful
Dojo對集合層次和實體層次的資料模型有一個清晰的界限,Dojo的store提供了集合層次的模型,接下來我們來看看實體層次的物件模型。Dojo對獨立的對象也使用了統一的介面。這裡我們可以使用dojo/Stateful的介面與對象互動。介面很簡單,主要有三個方法:
get(name) - 檢索屬性值
set(name, value) - 設定屬性值
watch(name, listener) - 註冊一個監聽屬性變化的callback方法(不設第一個參數則表示監聽所有屬性改動)
和之前介紹的viewer綁定資料一樣,他能及時反映相應的資料變化。首先,我們先建立一個viewer,綁定到一個對象:
<form id="itemForm"> Name: <input type="text" name="name" /> Quantity: <input type="text" name="quantity" /></form>
然後,我們綁定到HTML:
function viewInForm(object, form){ // copy initial values into form inputs for(var i in object){ updateInput(i, null, object.get(i)); } // watch for any future changes in the object object.watch(updateInput); function updateInput(name, oldValue, newValue){ var input = query("input[name=" + name + "]", form)[0]; if(input){ input.value = newValue; } }}
現在我們可以通過store裡面的一個對象初始化form:
require(["dojo/Stateful", "dojo/_base/Deferred"], function(Stateful, Deferred){ Deferred.when(store.get("Donut"), function(item){ item = new Stateful(item); // wrap with stateful viewInForm(item, dom.byId("itemForm")); });
現在我們如果通過controller代碼修改這個對象,在viewer上就會馬上反映出來。
item.set("quantity", 4);
在這個例子中,我們可能也想加入onchange事件用於監聽使用者輸入進而修改對象本身,以達到一個雙向的綁定。(object的改動會反映到form上,同樣form的改動也會影響object)。Dojo的form manager(dojox.form.manager)同樣也提供了很多進階的互動功能。
接下來我們還要記住將修改了的對象傳回store裡面,如下:
on(saveButton, "click", function(){ inventoryStore.put(currentItem); // save the current state of the Stateful item});總結
通過使用Dojo的store架構和stateful(有狀態的)介面,我們便有了構建我們MVC應用的利器。Viewers能監聽資料的變化。Controllers能通過統一的介面來操作資料而不用知道資料的特殊結構,同時也不需要額外的代碼來操作viewer的變化。集合和實體的介面邊界清晰。所有這些都能協助您構建您自己的MVC應用。