chaojidan

导航

第四十五课: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编辑  收藏  举报