上一篇《淺談HTML5單頁面架構(一)——requirejs + angular + angular-route》探討了angular+requirejs的一個簡單架構,這一篇繼續來看看backbone如何跟requirejs結合。css
相同地,項目架構好與壞不是說用了多少牛逼的框架,而是怎麼合理利用框架,讓項目開發更流暢,代碼更容易管理。那麼帶着這個目的,咱們來繼續探討backbone。html
首先,來看看整個項目結構。java
跟上一篇angular相似,libs裏多了underscore和zepto。三個根目錄文件:jquery
第一步,仍是創建單頁面惟一的HTMLgit
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>Backbone & Requirejs</title> </head> <body> <div id="container"></div> <script data-baseurl="./" data-main="main.js" src="libs/require.js" id="main"></script> </body> </html>
backbone沒有在dom屬性上作文章,咱們仍是按原生的或者說熟悉的方法寫東西。這裏定義了一個container div做爲backbone的視圖。github
而後引入requirejs,data-main表示主程序入口。緩存
第二步,配置main.js服務器
(function (win) { //配置baseUrl var baseUrl = document.getElementById('main').getAttribute('data-baseurl'); /* * 文件依賴 */ var config = { baseUrl: baseUrl, //依賴相對路徑 paths: { //若是某個前綴的依賴不是按照baseUrl拼接這麼簡單,就須要在這裏指出 zepto: 'libs/zepto.min', jquery: 'libs/zepto.min', underscore: 'libs/underscore', backbone: 'libs/backbone', text: 'libs/text' //用於requirejs導入html類型的依賴 }, shim: { //引入沒有使用requirejs模塊寫法的類庫。backbone依賴underscore 'underscore': { exports: '_' }, 'jquery': { exports: '$' }, 'zepto': { exports: '$' }, 'backbone': { deps: ['underscore', 'jquery'], exports: 'Backbone' } } }; require.config(config); //Backbone會把本身加到全局變量中 require(['backbone', 'underscore', 'router'], function(){ Backbone.history.start(); //開始監控url變化 }); })(window);
關於requirejs的語法,仍是很少說,你們本身去官網看吧。這個是基礎。架構
使用backbone,不得不強調requirejs的shim配置。backbone這個土匪,要的東西多了,要給他「鞋」(underscore),還要給他美金$(jquery)。app
因爲終端使用jquery就太龐大了,因此這裏作了個小把戲,用zepto充當jquery,騙了土匪一把。用幾張越南盾,戲稱是美金,沒想到土老冒也信了。
有個地方須要注意的是,
不管在哪裏用requirejs引入backbone後,就會多了Backbone和$這兩個全局變量,因此後續再使用backbone就不須要拘束於requirejs的AMD寫法了。適當放鬆透透氣也是好的。
配置好依賴關係後,就能夠引入router,並調用關鍵的
Backbone.history.start
開始路由監控。
第三步,配置router,路由表
define(['backbone'], function () { var Router = Backbone.Router.extend({ routes: { 'module1': 'module1', 'module2(/:name)': 'module2', '*actions': 'defaultAction' }, //路由初始化能夠作一些事 initialize: function () { }, module1: function() { var url = 'module1/controller1.js'; //這裏不能用模塊依賴的寫法,而改成url的寫法,是爲了grunt requirejs打包的時候斷開依賴鏈,分開多個文件 require([url], function (controller) { controller(); }); }, //name跟路由配置裏邊的:name一致 module2: function(name) { var url = 'module2/controller2.js'; require([url], function (controller) { controller(name); }); }, defaultAction: function () { console.log('404'); location.hash = 'module2'; } }); var router = new Router(); router.on('route', function (route, params) { console.log('hash change', arguments); //這裏route是路由對應的方法名 }); return router; //這裏必須的,讓路由表執行 });
Backbone.Router.extend這個語法,相信就沒必要多說了,說多了也說不清楚,你們去官網纔是王道:http://backbonejs.org
backbone的路由寫法跟angular相似,但對於可選參數的寫法是不同的。angular使用:param?的方式,而backbone使用(:param),哪一個方式好,見仁見智吧。
這裏定義了一個默認路由,和兩個業務路由。
原理很簡單,就是遇到module1的哈希(hash)就執行後邊這個字符串對應的函數
估計你們早就知道這個玩意。而上述代碼中,關鍵不一樣點是,這裏利用了requirejs作了模塊化,路由跳轉後作的全部邏輯都在另外的js中定義。
關鍵的關鍵,這裏使用了url,並且是獨立變量的方式配置模塊的js,而不是
require(['module1/controller1'], function (controller) { controller(); });
目的是grunt作requirejs打包時,能切斷兩側的js,不要合併在一個大js中。
再另外,你們能夠善用一下router.on('route', function)這個接口,及時作一下事件解綁和一些清理工做。
第四步,寫一個簡單模塊
controller1.js
define(['module1/view1'], function (View) { var controller = function () { var view = new View(); view.render('kenko'); }; return controller; });
view1.js
define(['text!module1/tpl.html'], function (tpl) { var View1 = Backbone.View.extend({ el: '#container', initialize: function () { }, render: function (name) { this.$el.html(_.template(tpl, {name: name})); } }); return View1; });
tpl.html
<div> Here is module 1. My name: <%=name %><br> <a href="#module2">turn to module 2</a> </div>
模版的寫法跟angular不同,採用的是更廣泛的方式,jquery、underscore都是這個模式。簡單說就是<%%>包括js代碼,用=等號能夠直接輸出變量值。
這裏作了最簡單的MVC,M只是一個值name,C就是controller了,V就是view1。
View1的寫法須要遵循Backbone的語法,否則這裏用Backbone就沒意義了。el指向對應的視圖dom元素,用的是css選擇器,在View中可使用this.$el獲取到這個jquery風格變量。render是自定義的函數。
到這裏,運行程序,就能看到module1的效果了。
第五步,再寫第二個模塊,嚴格MVC的
model2.js
define([], function () { var Model2 = Backbone.Model.extend({ //模型默認的數據 defaults: function () { return { name: "noname" }; }, // 定義一些方法 fetch: function () { var o = this; //能夠作一些http請求 setTimeout(function(){ o.set({name:'vivi'}); o.trigger('nameEvent'); //向view觸發事件 }, 1000); } }); return Model2; });
Model的語法也是遵循Backbone要求了,defaults是默認屬性值。讀寫這些屬性,須要經過model.get/set接口,不然就是用toJSON返回整個對象,再否則就解剖式的使用model.attributes.xxx。
fetch是自定義方法,模擬http請求,這是很常規的作法了,不過這個例子沒使用backbone的rest化接口。
數據返回後,使用backbone內建的trigger觸發事件,通知監聽者,也就是view層了。backbone跟angular最大區別就是,backbone不關注view層的組件化,更關注的是model和事件機制,而angular則不重點提事件機制,採用雙向綁定把數據更新的破事隱藏起來。各有各的好處,見仁見智吧。
view2.js
define(['text!module2/tpl.html'], function (tpl) { var View2 = Backbone.View.extend({ el: '#container', events: { 'click button': 'clickSpan' //使用代理監聽交互,好處是界面即便從新rander了,事件還能觸發,不須要從新綁定。若是使用zepto手工逐個元素綁定,當元素刷新後,事件綁定就無效了 }, initialize: function () { this.model.on('nameEvent', this.render, this); //監聽事件 }, render: function () { this.$el.html(_.template(tpl, {name: this.model.get('name')})); //相似java的DAO思想,一切經過get set操做 }, clickSpan: function (e) { alert('you clicked the button'); } }); return View2; });
接着,咱們看看backbone一個典型視圖怎麼玩。先看initialize方法,這個是new View2()時先執行的初始化邏輯。
咱們在這裏監聽nameEvent這個消息,也就是model2拋出的事件。收到這個通知,就更新界面。邏輯很簡單。
這裏有一個比較好用的events,交互事件代理機制。
咱們不須要單獨的寫zepto on對dom分別綁定事件,只須要在這裏配置一個events映射表便可。
click button等同於zepto的
$('button').on('click', function)
這裏綁定的就是clickSpan事件。
這個事件代理機制,好處是,在路由切換的時候,能夠輕鬆移除事件監聽。
view.undelegateEvents()
tpl.html
<div> Here is module 2. My name: <%=name %><br> <button>click me!</button> <a href="#module1">turn to module 1</a> </div>
controller2.js
define(['module2/model2', 'module2/view2'], function (Model, View) { var controller = function (name) { var model = new Model(); name && model.set({ name:name //設置默認的屬性值 }); var view = new View({model:model}); view.render(); //利用Model定義的默認屬性初始化界面 model.fetch(); //拉取cgi等等,獲取數據,再觸發事件,界面收到消息作相應的動做 }; return controller; });
controller負責的作的事就是揉合數據,放到view中。先讓view用默認數據渲染,再讓model去拉取最新數據,最後經過事件機制更新界面。
固然,這個controller並非backbone規範,你們能夠盡情發揮。
最後回到路由表中,當hash變成module2時,就執行:
module2: function(name) { var url = 'module2/controller2.js'; require([url], function (controller) { controller(name); }); },
至此,簡單的requirejs+backbone框架已經完成了。除了router的耦合度很高外,每一個模塊邏輯代碼都已經獨立,app能夠輕鬆實現按需加載。
那麼追求機制的騷年,要停下來嗎?按這個方案,在實際開發中,多人常常會在router這個節骨眼上打架,這裏的配置化還不夠完美。
第六步,優化router,完全配置化
現有方案的問題是,router中除了寫路由配置外,還須要添加相應的function,這樣既冗餘又容易衝突,那麼可否監聽route事件,作一個統一的路由處理器?一個處理函數,處理所有路由響應。
define(['backbone'], function () { var routesMap = { 'module1': 'module1/controller1.js', //原來應該是一個方法名,這裏取巧改成模塊路徑 'module2(/:name)': 'module2/controller2.js', '*actions': 'defaultAction' }; var Router = Backbone.Router.extend({ routes: routesMap, defaultAction: function () { console.log('404'); location.hash = 'module2'; } }); var router = new Router(); //完全用on route接管路由的邏輯,這裏route是路由對應的value router.on('route', function (route, params) { require([route], function (controller) { if(router.currentController && router.currentController !== controller){ router.currentController.onRouteChange && router.currentController.onRouteChange(); } router.currentController = controller; controller.apply(null, params); //每一個模塊約定都返回controller }); }); return router; });
上述代碼,把路由表抽離,目的是能夠放到index.html中,能夠在服務器作直出,保持0緩存,輕鬆實現對外網版本的控制。
另外Router中,沒有了每一個路由對應的函數,而路由表中的key/value改成真正意義的一個字符串——模塊路徑。
感謝backbone的健壯,我開始還覺得這樣確定會報錯,結果backbone沒找到對應函數就中止執行了,不錯,贊一個。
沒有了一個個的相應函數,取而代之的是route事件處理器。
處理器中,利用了配置表的value,拉取對應的模塊,並調用相應的controller。有了這個小把戲,你們能夠自由發揮了,配置成各類字符串,多個controller集合在一個requirejs模塊中等等。。。
另外,這裏約定controller中有onRouteChange的接口,用於接收路由切換的通知,好作一些銷燬工做。
來看看新的controller代碼:
define(['module2/model2', 'module2/view2'], function (Model, View) { var controller = function (name) { var model = new Model(); name && model.set({ name:name //設置默認的屬性值 }); var view = new View({model:model}); view.render(); //利用Model定義的默認屬性初始化界面 model.fetch(); //拉取cgi等等,獲取數據,再觸發事件,界面收到消息作相應的動做 controller.onRouteChange = function () { console.log('change'); //能夠作一些銷燬工做,例如view.undelegateEvents() view.undelegateEvents(); }; }; return controller; });
至此,大功告成,多人開發中,須要修改路由,只須要修改一個配置,不在這裏寫任何邏輯,利用svn合併功能,輕鬆完成協同開發。
本文代碼:https://github.com/kenkozheng/HTML5_research/tree/master/BackboneRequireJS
下一篇:淺談HTML5單頁面架構(三)—— 迴歸本真:自定義路由 + requirejs + zepto + underscore