第四十五課:MVC,MVP,MVVM的區別

前端架構從MVC到MVP,再到MVVM,它們都有不一樣的應用場景。但MVVM已經被證明爲界面開發最好的方案了。javascript

MVP 是從經典的模式MVC演變而來,它們的基本思想有相通的地方:Controller/Presenter負責邏輯的處理,Model提供數據,View負 責顯示。做爲一種新的模式,MVP與MVC有着一個重大的區別:在MVP中View並不直接使用Model,它們之間的通訊是經過Presenter來進行的,全部的交互都發生在Presenter內部,而在MVC中View會直接Model中讀取數據而不是經過 Controller。css

MVC裏,View是能夠直接訪問Model的!從而,View裏會包含Model信息,不可避免的還要包括一些業務邏輯。 MVC模型關注的是Model的不變,因此,在MVC模型裏,Model不依賴於View,可是 View是依賴於Model的。不只如此,由於有一些業務邏輯在View裏實現了,致使要更改View也是比較困難的,至少那些業務邏輯是沒法重用的。html

在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很大的不一樣之處。java

MVVM在概念上是真正將頁面與數據邏輯分離的模式,它把數據綁定工做放到一個JS裏去實現,而這個JS文件的主要功能是完成數據的綁定,即把model綁定到UI的元素上。jquery

你們都知道,咱們前端使用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等。app

瞭解完這些概念後,咱們來看兩個用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的區別。

 

 

 

加油!

相關文章
相關標籤/搜索