上一篇《浅谈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