JavaScriptMVC的Model的相關外掛程式提供了很多工具用來更好的組織模型資料,例如驗證、關聯、列表等。但是更核心的功能是服務封裝、類型轉換和事件。
屬性和可觀察性
對於模型層最重要的就是擷取和設定模型的屬性,還有就是監聽模型執行個體得變化。可以使用觀察者模式解決這一問題,MVC使用視圖去監視模型的改變。
javascriptMVC很容易實現這種可觀察性。一個很好的例子就是分頁,在一個頁面中經常存在好幾個分頁控制項,一個控制項提供上一頁和下一頁的按鈕,另一個控制項提供當前頁顯示的詳細資料(例如:"正在顯示 1-20 條"),所有控制項都需要完全相同的資料:
- offset - 顯示的第一條資料的索引
- limit - 每頁資訊顯示數
- count - 資訊總條數
我們可以使用$.Model建立這個模型資料:
var paginate = new $.Model({ offset: 0, limit: 20, count: 200});
現在paginate變數就可以被查看了。可以把它傳遞給那些對他執行讀寫操作,或者監視其屬性變化的分頁控制項。你可以使用傳統方式或者使用model.attr(Name)方法訪問paginate的屬性:
assertEqual( paginate.offset, 0 );assertEqual( paginate.attr('limit') , 20 );
當我們點擊下一頁的時候需要增加offset的值,可以使用model.attr(NAME, VALUE)改變屬性值。下面的代碼實現翻頁:
paginate.attr('offset',20);
當paginate的狀態被一個控制項修改的時候,另外的控制項需要被通知到。你可以使用model.bind(ATTR, success( ev, newVal ) )綁定特性屬性的變化,然後更新控制項:
paginate.bind('offset', function(ev, newVal){ $('#details').text( 'Showing items ' + (newVal+1 )+ '-' + this.count )})
你也可以綁定'updated.attr'來監聽任何屬性的變化:
paginate.bind('updated.attr', function(ev, newVal){ $('#details').text( 'Showing items ' + (newVal+1 )+ '-' + this.count )})
下面的代碼是接受paginate資料的next-previous外掛程式的代碼:
$.fn.nextPrev = function(paginate){ this.delegate('.next','click', function(){ var nextOffset = paginate.offset+paginate.limit; if( nextOffset < paginate.count){ paginate.attr('offset', nextOffset ); } }) this.delegate('.prev','click', function(){ var nextOffset = paginate.offset-paginate.limit; if( 0 < paginate.offset ){ paginate.attr('offset', Math.max(0, nextOffset) ); } }); var self = this; paginate.bind('updated.attr', function(){ var next = self.find('.next'), prev = self.find('.prev'); if( this.offset == 0 ){ prev.removeClass('enabled'); } else { prev.removeClass('disabled'); } if( this.offset > this.count - this.limit ){ next.removeClass('enabled'); } else { next.removeClass('disabled'); } })};
這個外掛程式也存在一些問題,首先如果控制項從頁面上移除,他不能從paginate上取消綁定。我們會在Controller章節中討論。其次存在判斷offset,防止他為負數或超過最大值的邏輯,這塊邏輯應該放在model中。我們需要添加其他的判斷,限制limit,offset。我們需要建立一個pagination類。
擴充Model
JavascriptMVC的Model繼承自$.Class,我們建立一個model類,通過繼承$.Model(NAME, [STATIC,] PROTOTYPE)的方式實現:
$.Model('Paginate',{ staticProperty: 'foo'},{ prototypeProperty: 'bar'})
有很多方式能讓Paginate模型變得更有用,首先可以添加setter方法,可以限制offset、count的值。
Setters
setter方法是model的原型方法,他被命名為setNAME,他有三個參數:val(被傳遞給model.attr(NAME, val)),success和error回呼函數。一般而言,方法返回要設定給model執行個體的值,或者當異常發生的時候執行error方法。當執行非同步setter的時候,執行success方法。下面的例子添加了setCount和setOffset執行個體方法,並且防止屬性被設定為負數。
$.Model('Paginate',{ setCount : function(newCount, success, error){ return newCount < 0 ? 0 : newCount; }, setOffset : function(newOffset, success, error){ return newOffset < 0 ? 0 : Math.min(newOffset, !isNaN(this.count - 1) ? this.count : Infinity ) }})
現在nextPrev外掛程式可是使用下面的方式設定offset的值:
this.delegate('.next','click', function(){ paginate.attr('offset', paginate.offset+paginate.limit);})this.delegate('.prev','click', function(){ paginate.attr('offset', paginate.offset-paginate.limit );});
預設值
我們可以設定靜態defaults屬性來為Paginate的執行個體設定預設值,當新執行個體被建立的時候,如果沒有設定其他值,執行個體將會使用預設值。
$.Model('Paginate',{ defaults : { count: Infinity, offset: 0, limit: 100 }},{ setCount : function(newCount, success, error){ ... }, setOffset : function(newOffset, success, error){ ... }})var paginate = new Paginate({count: 500});assertEqual(paginate.limit, 100);assertEqual(paginate.count, 500);
現在Paginate已經很有型了,但是通過添加輔助方法他會變得更棒。
輔助方法
輔助方法是原型方法,他用來在執行個體上擷取或者設定有用的資料。下面的例子中,Paginate包含了next和prev方法,用來導航到不同的頁面。他還提供了canNext和canPrev方法,用來判斷是否可以翻頁。
$.Model('Paginate',{ defaults : { count: Infinity, offset: 0, limit: 100 }},{ setCount : function( newCount ){ return Math.max(0, newCount ); }, setOffset : function( newOffset ){ return Math.max( 0 , Math.min(newOffset, this.count ) ) }, next : function(){ this.attr('offset', this.offset+this.limit); }, prev : function(){ this.attr('offset', this.offset - this.limit ) }, canNext : function(){ return this.offset > this.count - this.limit }, canPrev : function(){ return this.offset > 0 }})
現在我們的jquery外掛程式越來越有型了。
$.fn.nextPrev = function(paginate){ this.delegate('.next','click', function(){ paginate.attr('offset', paginate.offset+paginate.limit); }) this.delegate('.prev','click', function(){ paginate.attr('offset', paginate.offset-paginate.limit ); }); var self = this; paginate.bind('updated.attr', function(){ self.find('.prev')[paginate.canPrev() ? 'addClass' : 'removeClass']('enabled') self.find('.next')[paginate.canNext() ? 'addClass' : 'removeClass']('enabled'); })};
服務封裝
我們已經看到了$.Model對於模型化用戶端狀態時多麼有用,但是對於大多數程式來說,資料都在伺服器端,而不在用戶端。用戶端需要create, retrieve, update 和 delete (CRUD) 伺服器上的資料。維護用戶端和伺服器端的資料是件棘手的事情,而$.Model可以很好的解決這個問題。$.Model相當的靈活,他可以處理任意服務類型和資料類型,我們這裡只介紹具象狀態傳輸(REST) 和 JSON。
REST服務使用urls 和 HTTP 傳輸協議 POST, GET, PUT, DELETE分別實現對資料的create, retrieve, update, 和 delete。例如一個允許你create, retrieve, update 和delete任務的任務服務看起來會是這樣:
| 動作 |
動詞(verb) |
URL |
BODY |
響應 |
| Create a task |
POST |
/tasks |
name=do the dishes |
{ "id" : 2, "name" : "do the dishes", "acl" : "rw" , "createdAt": 1303173531164 // April 18 2011}
|
| Get a task |
GET |
/task/2 |
|
{ "id" : 2, "name" : "do the dishes", "acl" : "rw" , "createdAt": 1303173531164 // April 18 2011}
|
| Get tasks |
GET |
/tasks |
|
[{ "id" : 1, "name" : "take out trash", "acl" : "r", "createdAt": 1303000731164 // April 16 2011},{ "id" : 2, "name" : "do the dishes", "acl" : "rw" , "createdAt": 1303173531164 // April 18 2011}]
|
| Update a task |
PUT |
/task/2 |
name=take out recycling |
{ "id" : 2, "name" : "take out recycling", "acl" : "rw" , "createdAt": 1303173531164 // April 18 2011}
|
| Delete a task |
DELETE |
/task/2 |
|
{}
|
下面的代碼串連到伺服器,允許我們在伺服器上create, retrieve, update 和 delete任務。
$.Model("Task",{ create : "POST /tasks.json", findOne : "GET /tasks/{id}.json", findAll : "GET /tasks.json", update : "PUT /tasks/{id}.json", destroy : "DELETE /tasks/{id}.json"},{ });
下面的代碼展示了,我們如何使用task去CRUD任務。
| 動作 |
代碼 |
描述 |
| Create a task |
new Task({ name: 'do the dishes'}) .save( success( task, data ), error( jqXHR) ) -> taskDeferred
|
在伺服器端建立model執行個體, 首先使用 new Model(attributes). 然後執行 save(). Save的時候判斷 task 是否包含 id. 在這個例子中,我們不使用task的屬性去建立請求儲存資料,我們給 Save提供兩個參數:
success - 執行成功時執行,從伺服器擷取到task執行個體和資料。
error - 發生異常時執行。
Save 返回一個延時來解決任務建立。 |
| Get a task |
Task.findOne(params, success( task ), error( jqXHR) ) -> taskDeferred
|
從伺服器擷取單個執行個體,提供三個參數:
params - 傳遞給伺服器的資料,例如:{id: 2}.
success - 執行成功時執行,從伺服器擷取到task執行個體和資料。
error - 發生異常時執行。
findOne 返回延時解決task。 |
| Get tasks |
Task.findAll(params, success( tasks ), error( jqXHR) ) -> tasksDeferred
|
從伺服器返回task數組,採用三個參數:
params - data to pass to the server. Typically, it's an empty object ({}) or filters:{limit: 20, offset: 100}.
success - 執行成功時執行,從伺服器擷取到task執行個體和資料。
error - 發生異常時執行。
findOne 返回延時解決tasks數組。 |
| Update a task |
task.attr('name','take out recycling');task.save( success( task, data ), error( jqXHR) ) -> taskDeferred
|
在伺服器上執行更新, 首先修改模型執行個體的attr,然後執行save()。Save使用相同的參數,返回與建立task相同的延時。 |
| Destroy a task |
task.destroy( success( task, data ), error( jqXHR) ) -> taskDeferred
|
從伺服器銷毀task,採用兩個參數:
success - 執行成功時執行,從伺服器擷取到task執行個體和資料。
error - 發生異常時執行。
Destroy 返回延時用於銷毀task。
|
Task模型變成了我們服務的契約。
類型轉換
Javascript編程的時候經常會遇到時間轉換的難題,例如1303173531164代表的是April 18th, 2011,我們需要使用new Date(1303173531164)的方式來解決問題。幸運的是,$.Model為我們提供了類型轉換的功能,可以很方便的解決這個問題。我們需要定義一個類型的屬性和相應的轉換函式,類型屬性放在靜態對象attributes中,轉換函式放在靜態對象convert中,如下:
$.Model('Task',{ attributes : { createdAt : 'date' }, convert : { date : function(date){ return typeof date == 'number' ? new Date(date) : date; } }},{});
我們可以使用下面的方式使用:
Task.findAll({}, function(tasks){ $.each(tasks, function(){ console.log( "Year = "+this.createdAt.fullYear() ) })});
CRUD事件
當執行個體被created, updated, 或 destroyed的時候,模型會發布事件。你可以在Model上或者在他的執行個體上監聽事件,使用MODEL.bind(EVENT, callback( ev, instance ) )去監聽created, updated, 或 destroyed事件。當我們想知道task被建立和添加到頁面上,或者他發生更新的時候,我們可以用下面的代碼實現:
Task.bind('created', function(ev, task){ var el = $('<li>').html(todo.name); el.appendTo($('#todos')); task.bind('updated', function(){ el.html(this.name) }).bind('destroyed', function(){ el.remove() })})
javascriptMVC教程目錄