Backbone簡單、靈活,無論是富JS應用或企業網站都用得上,相比React針對View和單向資料流的設計,Backbone更能體現MVC的思路,所以針對它寫一篇入門範例,需要的朋友可以參考下,希望能幫助大家。
11年剛開始用前端MVC框架時寫過一篇文章,當時Knockout和Backbone都在用,但之後的項目全是在用Backbone,主要因為它簡單、靈活,無論是富JS應用程式還是企業網站都用得上。相較於React針對View和單向資料流的設計,Backbone更能體現MVC的思路,所以針對它寫一篇入門範例,說明如下:
1. 結構上分4節,介紹Model/ View/Collection,實現從遠端取得資料顯示到表格且修改刪除;
2. 名為“範例”,所以程式碼為主,每節的第1段程式碼都是完整程式碼,複製貼上就能用,每段程式碼都是基於前一段程式碼來寫的,因此每段程式碼的新內容不會超過20行(大括號計算在內);
3. 每行程式碼沒有註釋,但重要內容之後有寫具體的說明;
4. 開發環境是Chrome,使用github的API,這樣用Chrome即使在本地路徑(形如file://的路徑)也能取得資料。
0. Introduction
幾乎所有的框架都是在做兩件事:一是幫你把程式碼寫在正確的地方;二是幫你做一些髒活累活。 Backbone實現一個清晰的MVC程式碼結構,解決了資料模型和視圖映射的問題。雖然所有JS相關的項目都可以用,但Backbone最適合的還是這樣一種場景:需要用JS生成大量的頁面內容(HTML為主),用戶跟頁面元素有很多的交互行為。
Backbone物件有5個重要的函數,Model/Collection/View/Router/History。 Router和History是針對Web應用程式的最佳化,建議先熟悉pushState的相關知識。入門階段可以只了解Model/Collection/View。將Model視為核心,Collection是Model的集合,View是為了實現Model的改變在前端的反映。
1. Model
Model是所有JS應用程式的核心,很多Backbone教學喜歡從View開始講,其實View的內容不多,而且理解了View意義不大,理解Model更重要。以下程式碼實作從github的API取得一個gist訊息,顯示到頁面上:
<!DOCTYPE html> <html> <head> <script type="text/javascript" src="https://code.jquery.com/jquery-1.11.1.js"></script> <script type="text/javascript" src="http://underscorejs.org/underscore-min.js"></script> <script type="text/javascript" src="http://backbonejs.org/backbone-min.js"></script> <link href="http://cdn.bootcss.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="stylesheet"> </head> <body> <table id="js-id-gists" class="table"> <thead><th>description</th><th>URL</th><th>created_at</th></thead> <tbody></tbody> </table> <script type="text/javascript"> var Gist = Backbone.Model.extend({ url: 'https://api.github.com/gists/public', parse: function (response) { return (response[0]); } }), gist = new Gist(); gist.on('change', function (model) { var tbody = document.getElementById('js-id-gists').children[1], tr = document.getElementById(model.get('id')); if (!tr) { tr = document.createElement('tr'); tr.setAttribute('id', model.get('id')); } tr.innerHTML = '<td>' + model.get('description') + '</td><td>' + model.get('url') + '</td><td>' + model.get('created_at') + '</td>'; tbody.appendChild(tr); }); gist.fetch(); </script> </body> </html>
LINE4~8: 載入要用到的JS函式庫。 ajax請求和部分View的功能需要jQuery支援(或重寫ajax/View的功能);Backbone的程式碼是基於Underscore寫的(或用Lo-Dash取代);載入bootstrap.css只是因為預設樣式太難看…
LINE16~22: 建立一個Model並且實例化。 url是資料來源(API介面)的位址,parse用來處理傳回的資料。實際回傳的是一個Array,這裡取第一個Object。
LINE24~33: 綁定change事件。還沒使用View,所以要自己處理HTML。這10行程式碼主要是get的用法(model.get),其他的功能之後會用View來實作。
LINE34: 執行fetch。從遠端取得數據,獲到數據後會觸發change事件。可以重寫sync方法
開啟Chrome的Console,輸入gist,可以看到Model獲得的屬性:
Model提供資料和與資料相關的邏輯。上圖輸出的屬性是數據,程式碼中的fetch/parse/get/set都是對數據進行操作,其他的還有escape/unset/clear/destory,從函數名字就大致可以明白它的用途。還有很常用的validate函數,在set/save操作時用來做資料驗證,驗證失敗會觸發invalid事件:
/* 替换之前代码的JS部分(LINE16~34) */ var Gist = Backbone.Model.extend({ url: 'https://api.github.com/gists/public', parse: function (response) { return (response[0]); }, defaults: { website: 'dmyz' }, validate: function (attrs) { if (attrs.website == 'dmyz') { return 'Website Error'; } } }), gist = new Gist(); gist.on('invalid', function (model, error) { alert(error); }); gist.on('change', function (model) { var tbody = document.getElementById('js-id-gists').children[1], tr = document.getElementById(model.get('id')); if (!tr) { tr = document.createElement('tr'); tr.setAttribute('id', model.get('id')); } tr.innerHTML = '<td>'+ model.get('description') +'</td><td>'+ model.get('url') +'</td><td>'+ model.get('created_at') +'</td>'; tbody.appendChild(tr); }); gist.save();
跟之前的程式碼比較,有4處改動:
#LINE7~9: 增加了defaults。如果屬性中沒有website(注意不是website值為空),會設定website值為dmyz。
LINE10~14: 增加validate函數。當website值為dmyz時,觸發invalid事件。
LINE18~20: 綁定invalid事件,alert回傳的錯誤。
LINE31: 不做fetch,直接save操作。
因為沒有fetch,所以頁面上不會顯示資料。執行save操作,會呼叫validate函數,驗證失敗會觸發invalid事件,alert出錯誤提示。同時save操作也會向Model的URL發起一個PUT請求,github這個API沒有處理PUT,所以會回傳404錯誤。
在Console中輸入gist.set(‘description', ‘demo'),可以看到頁面元素也會有對應的變化。執行gist.set(‘description', gist.previous(‘description'))恢復之前的值。這就是Model和View的映射,現在還是自己實現的,下一節會用Backbone的View來實現。
2.View
用Backbone的View來改寫之前程式碼LINE24~33部分:
<!DOCTYPE html> <html> <head> <script type="text/javascript" src="https://code.jquery.com/jquery-1.11.1.js"></script> <script type="text/javascript" src="http://underscorejs.org/underscore-min.js"></script> <script type="text/javascript" src="http://backbonejs.org/backbone-min.js"></script> <link href="http://cdn.bootcss.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="stylesheet"> </head> <body> <table id="js-id-gists" class="table"> <thead><th>description</th><th>URL</th><th>created_at</th><th></th></thead> <tbody></tbody> </table> <script type="text/javascript"> var Gist = Backbone.Model.extend({ url: 'https://api.github.com/gists/public', parse: function (response) { return response[0]; } }), gist = new Gist(); var GistRow = Backbone.View.extend({ el: 'tbody', MODEL: gist, events: { 'click a': 'replaceURL' }, replaceURL: function () { this.MODEL.set('url', 'http://dmyz.org'); }, initialize: function () { this.listenTo(this.MODEL, 'change', this.render); }, render: function () { var model = this.MODEL, tr = document.createElement('tr'); tr.innerHTML = '<td>' + model.get('description') + '</td><td>' + model.get('url') + '</td><td>' + model.get('created_at') + '</td><td><a href="javascript:void(0)" rel="external nofollow" rel="external nofollow" rel="external nofollow" >®</a></td>'; this.el.innerHTML = tr.outerHTML; return this; } }); var tr = new GistRow(); gist.fetch(); </script> </body> </html>
LINE25: 所有的View都是基于DOM的,指定el会选择页面的元素,指定tagName会创建相应的DOM,如果都没有指定会是一个空的p。
LINE27~32: 绑定click事件到a标签,replaceURL函数会修改(set)url属性的值。
LINE33~35: View的初始化函数(initialize),监听change事件,当Model数据更新时触发render函数。
LINE36~42: render函数。主要是LINE41~42这两行,把生成的HTML代码写到this.el,返回this。
LINE44: 实例化GistRow,初始化函数(initialize)会被执行。
点击行末的a标签,页面显示的这条记录的URL会被修改成http://dmyz.org。
这个View名为GistRow,选择的却是tbody标签,这显然是不合理的。接下来更改JS代码,显示API返回的30条数据:
/* 替换之前代码的JS部分(LINE16~45) */ var Gist = Backbone.Model.extend(), Gists = Backbone.Model.extend({ url: 'https://api.github.com/gists/public', parse: function (response) { return response; } }), gists = new Gists(); var GistRow = Backbone.View.extend({ tagName: 'tr', render: function (object) { var model = new Gist(object); this.el.innerHTML = '<td>' + model.get('description') + '</td><td>'+ model.get('url') + '</td><td>' + model.get('created_at') + '</td><td></td>' return this; } }); var GistsView = Backbone.View.extend({ el: 'tbody', model: gists, initialize: function () { this.listenTo(this.model, 'change', this.render); }, render: function () { var html = ''; _.forEach(this.model.attributes, function (object) { var tr = new GistRow(); html += tr.render(object).el.outerHTML; }); this.el.innerHTML = html; return this; } }); var gistsView = new GistsView(); gists.fetch();
LINE2~9: 创建了两个Model(Gist和Gists),parse现在返回完整Array而不只是第一条。
LINE11~18: 创建一个tr。render方法会传一个Object来实例化一个Gist的Model,再从这个Model里get需要的值。
LINE26~34: 遍历Model中的所有属性。现在使用的是Model而不是Collection,所以遍历出的是Object。forEach是Underscore的函数。
Backbone的View更多的是组织代码的作用,它实际干的活很少。View的model属性在本节第一段代码用的是大写,表明只是一个名字,并不是说给View传一个Model它会替你完成什么,控制逻辑还是要自己写。还有View中经常会用到的template函数,也是要自己定义的,具体结合哪种模板引擎来用就看自己的需求了。
这段代码中的Gists比较难操作其中的每一个值,它其实应该是Gist的集合,这就是Backbone的Collection做的事了。
3. Collection
Collection是Model的集合,在这个Collection中的Model如果触发了某个事件,可以在Collection中接收到并做处理。第2节的代码用Collection实现:
<!DOCTYPE html> <html> <head> <script type="text/javascript" src="https://code.jquery.com/jquery-1.11.1.js"></script> <script type="text/javascript" src="http://underscorejs.org/underscore-min.js"></script> <script type="text/javascript" src="http://backbonejs.org/backbone-min.js"></script> <link href="http://cdn.bootcss.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="stylesheet"> </head> <body> <table id="js-id-gists" class="table"> <thead><th>description</th><th>URL</th><th>created_at</th><th></th></thead> <tbody></tbody> </table> <script type="text/javascript"> var Gist = Backbone.Model.extend(), Gists = Backbone.Collection.extend({ model: Gist, url: 'https://api.github.com/gists/public', parse: function (response) { return response; } }), gists = new Gists(); var GistRow = Backbone.View.extend({ tagName: 'tr', render: function (model) { this.el.innerHTML = '<td>' + model.get('description') + '</td><td>'+ model.get('url') + '</td><td>' + model.get('created_at') + '</td><td></td>' return this; } }); var GistsView = Backbone.View.extend({ el: 'tbody', collection: gists, initialize: function () { this.listenTo(this.collection, 'reset', this.render); }, render: function () { var html = ''; _.forEach(this.collection.models, function (model) { var tr = new GistRow(); html += tr.render(model).el.outerHTML; }); this.el.innerHTML = html; return this; } }); var gistsView = new GistsView(); gists.fetch({reset: true}); </script> </body> </html>
LINE17~23: 基本跟第2节的第2段代码一样。把Model改成Collection,指定Collection的Model,这样Collectio获得返回值会自动封装成Model的Array。
LINE38: Collection和Model不同,获取到数据也不会触发事件,所以绑定一个reset事件,在之后的fetch操作中传递{reset: true}。
LINE42~45: 从Collection从遍历Model,传给GistRow这个View,生成HTML。
Collection是Backbone里功能最多的函数(虽然其中很多是Underscore的),而且只要理解了Model和View的关系,使用Collection不会有任何障碍。给Collection绑定各种事件来实现丰富的交互功能了,以下这段JS代码会加入删除/编辑的操作,可以在JSBIN上查看源代码和执行结果。只是增加了事件,没有什么新内容,所以就不做说明了,附上JSBIN的演示地址:http://jsbin.com/jevisopo/1
/* 替换之前代码的JS部分(LINE16~51) */ var Gist = Backbone.Model.extend(), Gists = Backbone.Collection.extend({ model: Gist, url: 'https://api.github.com/gists/public', parse: function (response) { return response; } }), gists = new Gists(); var GistRow = Backbone.View.extend({ tagName: 'tr', render: function (model) { this.el.id = model.cid; this.el.innerHTML = '<td>' + model.get('description') + '</td><td>'+ model.get('url') + '</td><td>' + model.get('created_at') + '</td><td><a href="javascript:void(0)" rel="external nofollow" rel="external nofollow" rel="external nofollow" class="js-remove">X</a> <a href="javascript:void(0)" rel="external nofollow" rel="external nofollow" rel="external nofollow" class="js-edit">E</a> </td>' return this; } }); var GistsView = Backbone.View.extend({ el: 'tbody', collection: gists, events: { 'click a.js-remove': function (e) { var cid = e.currentTarget.parentElement.parentElement.id; gists.get(cid).destroy(); gists.remove(cid); }, 'click a.js-edit': 'editRow', 'blur td[contenteditable]': 'saveRow' }, editRow: function (e) { var tr = e.currentTarget.parentElement.parentElement, i = 0; while (i < 3) { tr.children[i].setAttribute('contenteditable', true); i++; } }, saveRow: function (e) { var tr = e.currentTarget.parentElement, model = gists.get(tr.id); model.set({ 'description' : tr.children[0].innerText, 'url': tr.children[1].innerText, 'created_at': tr.children[2].innerText }); model.save(); }, initialize: function () { var self = this; _.forEach(['reset', 'remove', 'range'], function (e) { self.listenTo(self.collection, e, self.render); }); }, render: function () { var html = ''; _.forEach(this.collection.models, function (model) { var tr = new GistRow(); html += tr.render(model).el.outerHTML; }); this.el.innerHTML = html; return this; } }); var gistsView = new GistsView(); gists.fetch({reset: true});
Afterword
虽然是入门范例,但因为篇幅有限,有些基本语言特征和Backbone的功能不可能面面俱到,如果还看不懂肯定是我漏掉了需要解释的点,请(在Google之后)评论或是邮件告知。
Backbone不是jQuery插件,引入以后整个DOM立即实现增删改查了,也做不到KnockoutJS/AnglarJS那样,在DOM上做数据绑定就自动完成逻辑。它是将一些前端工作处理得更好更规范,如果学习前端MVC的目的是想轻松完成工作,Backbone可能不是最佳选择。如果有一个项目,100多行HTML和1000多行JS,JS主要都在操作页面DOM(如果讨厌+号连接HTML还可以搭配React/JSX来写),那就可以考虑用Backbone来重写了,它比其他庞大的MVC框架要容易掌握得多,作为入门学习也是非常不错的。
相关推荐:
javascript - 关于backbone.js里的model.set和model.get
以上是backbone.js 簡單入門實例的詳細內容。更多資訊請關注PHP中文網其他相關文章!