如何將模型-視圖-控制器 (MVC) 架構引入 Ajax Web 應用程序html
如何高效管理 Web 應用程序中的數目衆多的 JavaScript 代碼行是一個挑戰。Asynchronous JavaScript and XML (Ajax) 交互大量充斥着各類頁面,爲用戶提供了更好體驗。愈來愈廣泛的單頁界面均由 Ajax 驅動。Backbone 是一個 JavaScript 框架,可用於建立模型-視圖-控制器 (model-view-controller, MVC) 類應用程序和單頁界面。在本文中,咱們將瞭解 Backbone 如何用於建立 Ajax 應用程序或單頁界面。前端
Web 應用程序愈來愈關注於前端,使用客戶端腳本與 Ajax 進行交互。因爲 JavaScript 應用程序愈來愈複雜,若是沒有合適的工具和模式,那麼 JavaScript 代碼的高效編寫、非重複性和可維護性方面會面臨挑戰。模型-視圖-控制器 (MVC) 是一個常見模式,可用於服務器端開發以生成有組織以及易維護的代碼。MVC 支持將數據(好比一般用於 Ajax 交互的 JavaScript Object Notation (JSON) 對象)從表示層或從頁面的文檔對象模型 (document object model, DOM) 中分離出來,也可適用於客戶端開發。web
Backbone(也稱爲 Backbone.js)是由 Jeremy Ashkenas 建立的一個輕量級庫,可用於建立 MVC 類應用程序。Backbone:ajax
模型、視圖、集合和路由器是 Backbone 框架中的主要組件。在 Backbone 中,模型會存儲經過 RESTful JSON 接口從服務器檢索到的數據。模型與視圖密切關聯,負責爲特定 UI 組件渲染 HTML 並處理元素上觸發的事件,這也是視圖自己的一部分。瀏覽器
在本文中,咱們將學習幾個不一樣的 Backbone.js 框架組件。研究 MVC 如何應用於 Backbone。經過這些實例,看看在建立 Ajax 應用程序或者單頁界面 (SPI) 時 Backbone 是多麼的重要。服務器
下載 本文使用的源代碼。架構
回頁首app
Backbone.Router
和 Backbone.history
含有大量 Ajax 交互的應用程序愈來愈像那些無頁面刷新的應用程序。這些應用程序經常試圖限制與單個頁面的交互。該 SPI 方法提升了效率和速度,並使整個應用程序變得更靈敏。狀態概念代替了頁面概念。散列 (Hash) 片斷被用於識別一個特定狀態。散列片斷 是 URL 中散列標籤 (#) 後的那部分,是該類應用程序的關鍵元素。清單 1 顯示了一個 SPI 應用程序使用兩個不一樣的散列片斷產生的兩個不一樣狀態。框架
http://www.example.com/#/state1
http://www.example.com/#/state2
Backbone 提供一個稱爲路由器(版本 0.5 前稱之爲控制器)的組件來路由客戶端狀態。路由器能夠擴展 Backbone.Router
函數,且包含一個散列映射(routes
屬性)將狀態與活動關聯起來。當應用程序達到相關狀態時,會觸發一個特定活動。清單 2 展現了一個 Backbone 路由器示例。異步
Backbone.Router
示例:routers.jsApp.Routers.Main = Backbone.Router.extend({
// Hash maps for routes
routes : {
"" : "index",
"/teams" : "getTeams",
"/teams/:country" : "getTeamsCountry",
"/teams/:country/:name : "getTeam"
"*error" : "fourOfour"
},
index: function(){
// Homepage
},
getTeams: function() {
// List all teams
},
getTeamsCountry: function(country) {
// Get list of teams for specific country
},
getTeam: function(country, name) {
// Get the teams for a specific country and with a specific name
},
fourOfour: function(error) {
// 404 page
}
});
建立的每一個狀態能夠爲書籤。當 URL 碰到相似下面狀況時,會調用這 5 個活動(index
、getTeams
、getTeamsCountry
、getTeamCountry
和fourOfour
)。
http://www.example.com
觸發 index()
http://www.example.com/#/teams
觸發 getTeams()
http://www.example.com/#/teams/country1
觸發 getTeamsCountry()
傳遞 country1
做爲參數http://www.example.com/#/teams/country1/team1
觸發 getTeamCountry()
傳遞 country1
和 team1
做爲參數http://www.example.com/#/something
觸發 fourOfour()
以做 *
(星號)使用。要啓動 Backbone,先實例化頁面加載的路由器,並經過指令 Backbone.history.start()
方法監視散列片斷中的任何變動,如 清單 3 所示。
$(function(){
var router = new App.Routers.Main();
Backbone.history.start({pushState : true});
})
當實例化路由器時,會生成 Backbone.history
對象;它將自動引用 Backbone.History
函數。Backbone.History
負責匹配路由和 router
對象中定義的活動。start()
方法觸發後,將建立 Backbone.history
的 fragment
屬性。它包含散列片斷的值。該序列在根據狀態次序管理瀏覽器歷史方面十分有用。用戶若是想要返回前一狀態,單擊瀏覽器的返回按鈕。
在 清單 3 的示例中,經過一個啓用 HTML5 特性 pushState
的配置調用 start()
方法。對於那些支持 pushState
的瀏覽器,Backbone 將監視 popstate 事件以觸發一個新狀態。若是瀏覽器不能支持 HTML5 特性,那麼 onhashchange
活動會被監視。若是瀏覽器不支持該事件,輪詢技術將監視 URL 散列片斷的任何更改。
模型和集合是 Backbone.js 的重要組件,模型將數據(一般是來自服務器的數據)存儲在鍵值對中。要建立一個模型,須要擴展Backbone.Model
,如 清單 4 所示。
Backbone.Model
建立App.Models.Team = Backbone.Model.extend({
defaults : {
// default attributes
}
// Domain-specific methods go here
});
App.Models.Team
函數是一個新模型函數,可是必須建立一個實例才能在應用程序中使用特定模型,如 清單 5 所示。
var team1 = new App.Models.Team();
如今,變量 team1
有一個名爲 cid 的字段名,這是一個客戶端標識符,形式爲 "c" 再加上一個數字(例如,c0、c一、c2)。模型是經過存儲在散列映射中的屬性來定義的。屬性能夠在實例化時進行設置,或者使用 set()
方法設置。屬性值可經過 get()
方法檢索。清單 6 顯示瞭如何經過實例化或 get()
/set()
方法設置和獲取屬性。
// "name" attribute is set into the model
var team1 = new App.Models.Team({
name : "name1"
});
console.log(team1.get("name")); // prints "name1"
// "name" attribute is set with a new value
team1.set({
name : "name2"
});
console.log(team1.get("name")); //prints "name2"
當使用 JavaScript 對象時,使用 set()
方法建立或者設置屬性值的緣由並非顯而易見的。其中一個緣由是爲了更新此值,如 清單 7 所示。
team1.attributes.name = "name2";
爲了避免 使用 清單 7 中的代碼,使用 set()
是改變模型狀態並觸發其變動事件的惟一方法。使用 set()
提高封裝原則。清單 8 展現瞭如何將一個事件處理程序綁到發生變動的事件中。該事件處理程序包含一個 alert,在調用 set()
方法時會被觸發,如 清單 6 所示。可是,在使用 清單 7 中的代碼時不觸發 alert。
App.Models.Team = Backbone.Model.extend({
initialize : function(){
this.bind("change", this.changed);
},
changed : function(){
alert("changed");
}
});
Backbone 的另外一個優點是易於經過 Ajax 交互與服務器進行通訊。在模型上調用一個 save()
方法會經過 REST JSON API 異步將當前狀態保存到服務器。清單 9 展現了此示例。
save
方法barca.save();
save()
函數將在後臺委託給 Backbone.sync
,這是負責發出 RESTful 請求的組件,默認使用 jQuery 函數 $.ajax()
。因爲調用了 REST 風格架構,每一個 Create、Read、Update 或 Delete (CRUD) 活動均會與各類不一樣類型的 HTTP 請求(POST
、GET
、PUT
和 DELETE
)相關聯。首先保存模型對象,使用一個 POST
請求,建立一個標識符 ID,其後,嘗試發送對象到服務器,使用一個 PUT
請求。
當須要從服務器檢索一個模型時,請求一個 Read 活動並使用一個 Ajax GET
請求。這類請求使用 fetch()
方法。要肯定導入模型數據或者從中取出模型數據的服務器的位置:
url
屬性將是該位置的基礎,而且該模型 ID(不是 cid)會被附加以構成完整的 URL。urlroot
屬性被用做該位置的基礎清單 10 顯示瞭如何獲取一個模型。
Fetch()
方法var teamNew = new App.Models.Team({
urlRoot : '/specialTeams'
});
teamNew.save(); // returns model's ID equal to '222'
teamNew.fetch(); // Ajax request to '/specialTeams/222'
validate()
方法被用於驗證模型,如 清單 11 所示。須要重寫 validate()
方法(在調用 set()
方法時觸發)來包含模型的有效邏輯。傳遞給該函數的唯一參數是一個 JavaScript 對象,該對象包含了 set()
方法更新的屬性,以便驗證那些屬性的條件。若是從 validate()
方法中沒有返回任何內容,那麼驗證成功。若是返回一個錯誤消息,那麼驗證失敗,將沒法執行 set()
方法。
App.Models.Team = Backbone.Model.extend({
validate : function(attributes){
if (!!attributes && attributes.name === "teamX") {
// Error message returned if the value of the "name"
// attribute is equal to "teamX"
return "Error!";
}
}
}
一組模型被分組到到集合中,這個集合是 Backbone.Collection
的擴展函數。集合具備一個模型屬性的特性,定義了組成該集合的模型類型。使用 add()
/remove()
方法能夠將一個模型添加和移動到集合中。清單 12 顯示瞭如何建立和填充一個集合。
App.Collections.Teams = Backbone.Collection.extend({
model : App.Models.Team
});
var teams = new App.Collections.Teams();
// Add e model to the collection object "teams"
teams.add(team1);
teams.add(new App.Models.Team({
name : "Team B"
}));
teams.add(new App.Models.Team());
teams.remove(team1);
console.log(teams.length) // prints 2
建立的 teams
集合中包含一個含有兩個模型的陣列,存儲在模型屬性中。儘管,在典型 Ajax 應用程序中,會從服務器動態(不是人工)填充該集合。fetch()
方法能夠幫助完成此項任務,如 清單 13 所示,並將數據存儲到模型陣列中。
Fetch()
方法teams.fetch();
Backbone 中的集合擁有一個 url
屬性,定義了使用 Ajax GET 請求從服務器取出 JSON 數據的位置,如 清單 14 所示。
url
屬性和 fetch()
方法teams.url = '/getTeams';
teams.fetch(); //Ajax GET Request to '/getTeams'
Fetch()
方法屬於異步調用,所以,在等待服務器響應時,應用程序不會停止。在一些狀況下,要操做來自服務器的原始數據,可使用集合的 parse()
方法。正如 清單 15 所示。
parse()
方法App.Collections.Teams = Backbone.Collection.extend({
model : App.Models.Team,
parse : function(data) {
// 'data' contains the raw JSON object
console.log(data);
}
});
集合提供的另外一個有趣的方法是 reset()
,它容許將多個模型設置到一個集合中。reset()
方法能夠很是方便地將數據引導到集合中,好比頁面加載,來避免用戶等待異步調用返回。
Backbone 中的視圖與典型 MVC 方法的視圖不同。Backbone 視圖能夠擴展 Backbone.View
函數並顯示模型中存儲的數據。一個視圖提供一個由 el
屬性定義的 HTML 元素。該屬性能夠是由 tagName
、className
和 id
屬性相組合而構成的,或者是經過其自己的 el
值造成的。清單 16 顯示了使用這不一樣方法組合 el
屬性的兩個不一樣視圖。
// In the following view, el value is 'UL.team-element'
App.Views.Teams = Backbone.View.extend({
el : 'UL.team-list'
});
// In the following view, el value is 'div.team-element'
App.Views.Team = Backbone.View.extend({
className : '.team-element',
tagName : 'div'
});
若是 el
、tagName
、className
和 id
屬性爲空,那麼會默認將一個空的 DIV 分配給 el
。
如上所述,一個視圖必須與一個模型相關聯。該模型屬性也頗有用,如 清單 17 所示。App.View.Team
視圖被綁定到一個 App.Models.Team
模型實例。
// In the following view, el value is 'UL.team-element'
App.Views.Team = Backbone.View.extend({
...
model : new App.Models.Team
});
要渲染數據(這是視圖的主要目的),重寫 render()
方法和邏輯來顯示 DOM 元素(由 el
屬性引用的)中的模型屬性。清單 18 展現了一個 render 方法如何更新用戶界面的樣例。
Render()
方法App.Views.Team = Backbone.View.extend({
className : '.team-element',
tagName : 'div',
model : new App.Models.Team
render : function() {
// Render the 'name' attribute of the model associated
// inside the DOM element referred by 'el'
$(this.el).html("<span>" + this.model.get("name") + "</span>");
}
});
Backbone 也能夠促進客戶端模板的使用,這就使得咱們沒有必要在 JavaScript 中嵌入 HTML 代碼,如 清單 18 所示。(使用模板,模板會封裝視圖中常見函數;只指定此函數一次便可。)Backbone 在 underscore.js(一個必須的庫)中提供一個模板引擎,儘管沒有必要使用該模板引擎。清單 19 中的實例使用 underscore.js HTML 模板。
<script id="teamTemplate" type="text/template">
<%= name %>
</script>
清單 20 顯示了另外一個使用 underscore.js HTML 模板的樣例。
_.template()
函數的視圖App.Views.Team = Backbone.View.extend({
className : '.team-element',
tagName : 'div',
model : new App.Models.Team
render : function() {
// Compile the template
var compiledTemplate = _.template($('#teamTemplate').html());
// Model attributes loaded into the template. Template is
// appended to the DOM element referred by the el attribute
$(this.el).html(compiledTemplate(this.model.toJSON()));
}
});
Backbone 中最有用且最有趣的一個功能是將 render()
方法綁定到模型的變動事件中,如 清單 21 所示。
Render()
方法綁定到模型變動事件// In the following view, el value is 'div.team-element'
App.Views.Team = Backbone.View.extend({
model : new App.Models.Team,
initialize : function() {
this.model.bind("change", this.render, this);
}
})
上述代碼將 render()
方法綁定到一個模型的變動事件中。當模型發生更改時,會自動觸發 render()
方法,從而節省數行代碼。從 Backbone 0.5.2 開始,bind()
方法就開始接受使用第三個參數來定義回調函數的對象。(在上述示例中,當前視圖是回調函數 render()
中的對象)。在 Backbone 0.5.2 以前的版本中,必須使用 underscore.js 中的 bindAll
函數,如 清單 22 所示。
_.bindAll()
usage// In the following view, el value is 'div.team-element'
App.Views.Team = Backbone.View.extend({
initialize : function() {
_.bindAll(this, "render");
this.model.bind("change", this.render);
}
})
Backbone 視圖中,經過視圖中的 DOM 對象監聽事件是比較容易的。對於實現這一點,events
屬性非常方便的,如 清單 23 所示。
App.Views.Team = Backbone.View.extend({
className : '.team-element',
tagName : 'div',
events : {
"click a.more" : "moreInfo"
},
moreInfo : function(e){
// Logic here
}
})
events
屬性的每一個項均由兩部分構成:
在 清單 23 中,當用戶經過 DIV 中的類 more
以及類 team-element
點擊連接時,會調用函數 moreInfo
。
MVC 模式能夠爲大型 JavaScript 應用程序提供所需的組織化代碼。Backbone 是一個 JavaScript MVC 框架,它屬於輕量級框架,且易於學習掌握。模型、視圖、集合和路由器從不一樣的層面劃分了應用程序,並負責處理幾種特定事件。處理 Ajax 應用程序或者 SPI 應用程序時,Backbone 多是最好的解決方案。