第四十五课:MVC,MVP,MVVM的区别
前端架构从MVC到MVP,再到MVVM,它们都有不同的应用场景。但MVVM已经被证实为界面开发最好的方案了。
MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负 责显示。作为一种新的模式,MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter来进行的,所有的交互都发生在Presenter内部,而在MVC中View会直接从Model中读取数据而不是通过 Controller。
在MVC里,View是可以直接访问Model的!从而,View里会包含Model信息,不可避免的还要包括一些业务逻辑。 MVC模型关注的是Model的不变,所以,在MVC模型里,Model不依赖于View,但是 View是依赖于Model的。不仅如此,因为有一些业务逻辑在View里实现了,导致要更改View也是比较困难的,至少那些业务逻辑是无法重用的。
在MVP里,Presenter完全把Model和View进行了分离,主要的程序逻辑在Presenter里实现。而且,Presenter与具体的 View是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更View时候可以保持Presenter的不变,即重用!
在MVP里,应用程序的逻辑主要在Presenter来实现,其中的View是很薄的一层。在这个过程中,View是很简单的,能够把信息显示清楚就可以了。在后面,根据需要再随便更改View, 而对Presenter没有任何的影响了。 如果要实现的UI比较复杂,而且相关的显示逻辑还跟Model有关系,就可以在View和Presenter之间放置一个Adapter。由这个 Adapter来访问Model和View,避免两者之间的关联。而同时,因为Adapter实现了View的接口,从而可以保证与Presenter之间接口的不变。这样就可以保证View和Presenter之间接口的简洁,又不失去UI的灵活性。 在MVP模式里,View只应该有简单的Set/Get的方法,用户输入和设置界面显示的内容,除此就不应该有更多的内容,绝不容许直接访问 Model--这就是与MVC很大的不同之处。
MVVM在概念上是真正将页面与数据逻辑分离的模式,它把数据绑定工作放到一个JS里去实现,而这个JS文件的主要功能是完成数据的绑定,即把model绑定到UI的元素上。
大家都知道,我们前端使用MVC或MVP模式进行开发时,这个V与传统意义上的V是不一样的。在后端,这只是字符串的拼接,在前端,还涉及到DOM操作。即便你加入了模板,你也要将script标签中的模板内容与后端返回的数据进行结合,生成一个符合HTML结构的字符串,最后,通过innerHTML转换为页面节点,显示出来。而这些操作,我们可以通过MVVM中的动态模板搞定。它的原理大概是:动态模板在扫描之后,会得到所有要处理的节点的引用,这也意味着,以后我们要做一小部分的更新,不用像静态模板那样大规模替换,而是细化到每一个元素节点,特性节点或文本节点。这就是所谓的“最小化刷新”技术。一般的,只有ms-if等少量绑定才会影响到元素节点那一层面,更多的时候, 我们是在刷新特性节点的value值,文本节点的data值,这也意味着,我们的刷新不会引起reflow。加之,能得到元素节点本身,我们就可以轻松实现绑定事件,操作样式,修改属性等功能。这也是为什么大多数MVVM框架选择动态模板的缘故,jQuery原来可以做的,我们全部通过绑定属性或定界符在HTML里搞定。 这也意味着,我们实现了完美的分层架构,JS里面是纯粹的模型层(包括model与viewmodel),HTML里是视图层。
此外,MVVM另一个重要特性,双向绑定。它更方便你同时维护页面上都依赖于某个字段的N个区域,而不用手动更新它们。
有人做过测试:使用Angular(MVVM)代替Backbone(MVC)来开发,代码可以减少一半。
MVVM算一个很新的东西,后端诞生于2005年,前端诞生于2010年发布的knockout框架。目前主要有knockout.js,ember.js,angular.js,win.js,kendoui等。
了解完这些概念后,我们来看两个用Backbone写的例子,我们通过例子来详细的了解下前端MVC是如何实现的:
1 <!DOCTYPE html> 2 <html xmlns="http://www.w3.org/1999/xhtml"> 3 <head> 4 <meta charset="utf-8" /> 5 <title></title> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" /> 7 <link rel="Stylesheet" type="text/css" href="res/style/main2.css" /> 8 <link rel="Stylesheet" type="text/css" href="res/style/tuan.css" /> 9 <style> .pro_list_rank { margin: 5px 0; padding-right: 22px; } 10 .figcaption span { text-align: center; } 11 .blog_item {} 12 .blog_item img { width: 48px; height; 48px; margin: 4px; padding: 1px; float: left; border: 1px solid #CCC; } 13 14 .blog_item .item_footer { color: #757575; font-size: 0.86em; } 15 a { color: #005A94; } 16 .tab_hotel { border-left: 1px solid #2B97E2; } 17 .cont_wrap .content { background-color: White; padding: 5px 10px; } 18 img { max-width: 98%; }</style> 19 </head> 20 <body> 21 <div class="main-frame"> 22 <div class="main-viewport" id="main-viewport"> 23 </div> 24 </div> 25 <script type="text/template" id="index-template"> 26 <header> 27 <b class="icon_home i_bef" id="js_home"></b> 28 <h1> 29 博客园</h1> 30 31 <i id="js_return" class="returnico"></i> 32 </header> 33 <section class="cont_wrap"> 34 <div id="post"></div> 35 <ul class="pro_list" id="lstbox"> 36 </ul> 37 </section> 38 <ul class="tab_search fix_bottom" id="sort"> 39 <li class="tabcrt" attr="updated">时间</li> 40 <li class="tab_hotel" attr="diggs">推荐</li> 41 <li class="tab_hotel" attr="views">阅读</li> 42 <li class="tab_hotel" attr="comments">评论</li> 43 </ul> 44 </script> 45 <script type="text/template" id="item-template"> 46 <li class="arr_r orderItem" data-id="<%=id %>" data-index = "<%=index %>"> 47 <article class="blog_item"> 48 <h3> 49 <a href="<%=link.href %>" target="_blank"> 50 <%=title.value || '无题' %></a> 51 </h3> 52 <div class="author pro_list_rank"> 53 <%if(author.avatar){ %> 54 <a href="<%=author.uri %>" target="_blank"> 55 <img src="<%=author.avatar %>"> 56 </a> 57 <%} %> 58 <%=summary.value %> 59 </div> 60 <div class="item_footer"> 61 <a href="<%=author.uri %>" class="lightblue">Scut</a> 62 <%=published %> 63 <a href="<%=link.href %>" title="2013-08-21 15:21" class="gray">评论(<%=comments %>)</a> 64 <a href="<%=link.href %>" class="gray">阅读(<%=views %>)</a> <span class="price1">推荐(<%=diggs %>)</span></div> 65 </article> 66 </li> 67 </script> 68 <script type="text/template" id="detail-template"> 69 <section class="cont_wrap" > 70 <article class="content"> 71 <h1> 72 <a href="#"><%=title.value %></a></h1> 73 <div style=" text-align: right; "> 74 <time pubdate="pubdate" value="2013-04-15"><%=published %></time><br /><span>阅读(<%=views %>) 75 评论(<%=comments %>)</span> 76 </div> 77 <p><%=value %></p> 78 </article> 79 </section> 80 </script> 81 <script src="libs/jquery.js" type="text/javascript"></script> 82 <script src="libs/underscore.js" type="text/javascript"></script> 83 <script src="libs/backbone.js" type="text/javascript"></script> 84 <script type="text/javascript" src="libs/backbone.localStorage.js"></script> 85 <script type="text/javascript"> 86 //模型 87 var PostModel = Backbone.Model.extend({ 88 89 }); 90 91 //模型集合 92 var PostList = Backbone.Collection.extend({ 93 model: PostModel, 94 parse: function (data) { 95 96 return (data && data.feed && data.feed.entry) || {} 97 }, 98 setComparator: function (type) { 99 this.comparator = function (item) { 100 return Math.max(item.attributes[type]); 101 } 102 } 103 }); 104 //视图,文章内容的视图 105 var Detail = Backbone.View.extend({ 106 el: $('#main-viewport'), 107 template: _.template($('#index-template').html()), 108 detail: _.template($('#detail-template').html()), 109 initialize: function (app) { 110 this.app = app; 111 this.$el.html(this.template()); 112 this.wrapper = $('#lstbox'); 113 this.render(); 114 }, 115 render: function () { 116 var scope = this; 117 var id = this.app.id; 118 119 var param = { url: 'http://wcf.open.cnblogs.com/blog/post/body/' + id } 120 121 var model = this.app.model; 122 123 $.get('Handler.ashx', param, function (data) { 124 (typeof data === 'string') && (data = $.parseJSON(data)); 125 if (data && data.string) { 126 //此处将content内容写入model 127 model.set('value', data.string.value); 128 scope.wrapper.html(scope.detail(model.toJSON())); 129 } 130 }); 131 132 }, 133 events: { 134 'click #js_return': function () { 135 this.app.forward('index') 136 } 137 } 138 }); 139 //视图,文章列表的视图 140 var Index = Backbone.View.extend({ 141 el: $('#main-viewport'), 142 template: _.template($('#index-template').html()), 143 itemTmpt: _.template($('#item-template').html()), 144 145 events: { 146 'click #sort': function (e) { 147 var el = $(e.target); 148 var type = el.attr('attr'); 149 this.list.setComparator(type); 150 this.list.sort(); 151 }, 152 'click .orderItem': function (e) { 153 var el = $(e.currentTarget); 154 var index = el.attr('data-index'); 155 var id = el.attr('data-id'); 156 var model = this.list.models[index]; 157 this.app.model = model; 158 this.app.id = id; 161 this.app.forward('detail'); 175 } 176 }, 177 initialize: function (app) { 178 this.app = app; 179 180 //先生成框架html 181 this.$el.html(this.template()); 182 this.post = this.$('#post'); 183 184 var scope = this; 185 var curpage = 1; 186 var pageSize = 10; 187 this.list = new PostList(); 188 this.list.url = 'Handler.ashx?url=http://wcf.open.cnblogs.com/blog/sitehome/paged/' + curpage + '/' + pageSize; 189 this.list.fetch({ 190 success: function () { 191 scope.render(); 192 } 193 }); 194 this.wrapper = $('#lstbox'); 195 196 this.listenTo(this.list, 'all', this.render); 197 198 }, 199 render: function () { 200 201 var models = this.list.models; 202 var html = ''; 203 for (var i = 0, len = models.length; i < len; i++) { 204 models[i].index = i; 205 html += this.itemTmpt(_.extend(models[i].toJSON(), { index: i })); 206 } 207 this.wrapper.html(html); 208 var s = ''; 209 } 210 }); 215 var App = Backbone.Router.extend({ 216 routes: { 217 "": "index", // #index 218 "index": "index", // #index 219 "detail": "detail" // #detail 220 }, 221 index: function () { 222 var index = new Index(this.interface); 223 224 }, 225 detail: function () { 226 var detail = new Detail(this.interface); 227 228 }, 229 initialize: function () { 231 }, 232 interface: { 233 forward: function (url) { 234 window.location.href = ('#' + url).replace(/^#+/, '#'); 235 } 236 237 } 240 }); 242 var app = new App(); 243 Backbone.history.start(); 245 var s = ''; 247 </script> 248 </body> 249 </html>
我们来分析这段代码时,只需要看js代码。代码的一开始,我们先定义了一个模型PostModel,这个模型相当于后台返回的一条数据。然后定义了一个PostList集合,它里面的每一项就是模型PostModel。集合PostList有两个方法,一个是parse方法,它用于解析后台返回的数据,会自动调用,因此你可以重写此方法,改变后台数据的表现形式。第二个方法setComparator用来设置模型集合排序时,使用的比较方法(比如:模型集合PostList.sort(),会对里面的模型进行排序,这时排序调用的比较方法就是comparator)。
接下来,定义了一个视图Detail,此视图是用来显示文章内容的。由于它只显示一篇文章,所以它只操作一个模型,这里就是操作PostModel。
然后,定义了一个视图Index,此视图是用来显示文章列表的,由于它显示很多文章的标题,因此它操作的就是模型集合PostList。
最后定义了一个路由App,我们也可以叫它Controller。它主要通过Hash值的变化,来改变视图的。
我们总共定义了两个视图,一个模型,一个集合,一个路由。那我们如何使用他们呢,首先初始化一个路由对象,然后启动路由功能。路由的使用,我们不仅需要初始化一个对象,而且必须调用Backbone.history.start()。
当用户输入url访问这个页面的时候,比如:www.chaojidan.com,这时没有hash值,因此会调用路由中的index方法,这时,就会初始化Index视图,并把路由中的interface对象传进这个视图。实例化Index视图时,就会调用Index的initialize方法,在此方法中,又会实例化一个集合PostList对象list,然后通过这个集合对象向后台请求数据,数据返回后,就会存储在集合对象list中,这时就会调用视图Index的render方法,此方法,就会把集合list中的数据全部显示出来。同时,视图中的events对象,就会自动绑定一些事件。
当我们点击.orderItem这个元素(此元素就是文章列表)时,就会执行回调方法,此回调方法,就会让页面上显示此文章的内容,也就是视图的变化。在这个回调方法中,会调用路由的forward方法,此方法就会改变页面的url,这时url会变成www.chaojidan.com#detail。由于hash值变化了,这时就会调用路由中的回调方法detail,而此方法就会实例化一个detail视图对象。
在detail视图中,就会去获取你点击的文章的内容,然后显示在页面上。
大家看懂这个代码后,再来考虑下,它的MVC模式是如何体现的?
首先model模型PostModel,它对应后台的一条数据,collection集合PostList,它对应后台的多条数据。与后台交互的是collection,集合的功能就是从后台请求数据,然后把数据进行解析,每一条数据就是一个model。
然后视图Index是用来显示集合的的数据,也就是显示多个model。视图detail用来显示单条数据,这里的数据是文章内容,而collection集合中的数据是文章标题,也就是说在Index视图中,模型model只是一个文章标题,而在detail视图中,模型model是文章的内容。这里的视图是用模板的形式把数据套进去,然后添加到页面上的,每次模型的数据变化,都会进行模板重新组装,即便是改变了一个数据,就要把整个模板进行组装,是不是有点浪费呢?
视图之间的切换,是通过router路由来实现的,因为视图中绑定了一些方法,比如在文章列表中绑定了click事件,当你点击文章列表中的一项时(也就是想看此文章的内容时),就会改变hash值(改变hash的值,不会请求服务器),这时因为启动了路由功能,所以就会调用此hash值对应的方法,然后初始化detail视图,此视图,就会去后台取此文章的内容,然后显示在此页面上。
如果公司中的项目用Backbone来实现,然后加上sea.js来进行模块化开发,那么,我们可以在init.js中,引入路由这个模块,然后初始一个路由对象,并调用Backbone.historty.start()来启动此路由。而这个路由模块中,定义了一个跟菜单选项相对应的路由表,比如:第一个菜单,就是默认显示的,那么,它的hash值对应"",当用户访问www.chaojidan.com时,就会调用此hash对应的回调方法,然后加载此菜单需要的js文件,也就是模块(这里面其实就是定义了View和Model),这里通过sea.js中的require.async方法加载,加载成功后,就会实例化此View和Model,在View中就会进行初始化操作,然后就会通过model向服务器请求数据,最后通过View显示在页面中。
点击一个菜单,就会改变hash的值,就会执行相对应的回调方法, 然后就会加载相对应的js文件(模块),最后就会请求服务器返回数据,把数据显示在页面上。
这里的js文件(模块),只有你点击相对应的菜单栏时,才会去后台下载并解析,是否能够很好的处理同时加载太多js文件导致的页面假死情况。
这里面需要注意的是在js文件(模块)中,我们的initialize方法,一开始就需要调用thie.el.off()方法,此方法,就是取消此视图中的之前所有的事件绑定,以防你重复绑定。
这一课,在概念上,知道了MVC和MVVM的区别,然后从实际上知道了MVC的开发模式。
下一课,我们将从实际上来讲解MVC和MVVM的区别。
加油!
posted on 2015-01-16 10:57 chaojidan 阅读(2294) 评论(3) 编辑 收藏 举报