JavaScript MVC模式

2018-08-02 16:26 更新

MVC

MVC是一個架構設計模式,它通過分離關注點的方式來支持改進應用組織方式。它促成了業(yè)務數(shù)據(jù)(Models)從用戶界面(Views)中分離出來,還有第三個組成部分(Controllers)負責管理傳統(tǒng)意義上的業(yè)務邏輯和用戶輸入。該模式最初是由Trygve Reenskaug在研發(fā)Smalltalk-80 (1979)期間設計的,當時它起初被稱作Model-View-Controller-Editor。在1995年的“設計模式: 面向對象軟件中的可復用元素” (著名的"GoF"的書)中,MVC被進一步深入的描述,該書對MVC的流行使用起到了關鍵作用。

Smalltalk-80 MVC

了解一下最初的MVC模式打算解決什么問題是很重要的,因為自從誕生之日起它已經(jīng)發(fā)生了很大的改變。回到70年代,圖形用戶界面還很稀少,一個被稱為分離展示的概念開始被用來清晰的劃分下面兩種對象:領域對象,它對現(xiàn)實世界里的概念進行建模(比如一張照片,一個人), 還有展示對象,它被渲染到用戶屏幕上進行展示。

Smalltalk-80作為MVC的實現(xiàn),把這一概念進一步發(fā)展,產(chǎn)生這樣一個觀點,即把應用邏輯從用戶界面中分離開來。這種想法使得應用的各個部分之間得以解耦,也允許該應用中的其它界面對模型進行復用。關于Smalltalk-80的MVC架構,有幾點很有趣,值得注意一下:

  • 模型表現(xiàn)了領域特定的數(shù)據(jù),并且不用考慮用戶界面(視圖和控制器).當一個模型有所改變的時候,它會通知它的觀察者。
  • 視圖表現(xiàn)了一個模型的當前狀態(tài).觀察者模式被用來讓視圖在任何時候都知曉模型已經(jīng)被更新了或者被改變了。
  • 展現(xiàn)受到視圖的照管,但是不僅僅只有一個單獨的視圖或者控制器——每一個在屏幕上展現(xiàn)的部分或者元素都需要一個視圖-控制器對。
  • 控制器在這個視圖-控制器對中扮演著處理用戶交互的角色(比如按鍵或者點擊動作),做出對視圖的選擇。

開發(fā)者有時候會驚奇于他們了解到的觀察者模式(如今已經(jīng)被普遍的作為發(fā)布/訂閱的變異實現(xiàn)了)已經(jīng)在幾十年以前被作為MVC架構的一部分包含進去了.在Smalltalk-80的 MVC中,視圖觀察著模型.如上面要點中所提到的,模型在任何時候發(fā)生了改變,視圖就會做出響應.一個簡單的示例就是一個由股票市場數(shù)據(jù)支撐的應用程序——為了應用程序的實用性,任何對于我們模型中數(shù)據(jù)的改變都應該導致視圖中的結果實時的刷新。

Martin Fowler在過去數(shù)年完成了對原生MVC有關問題進行寫作的優(yōu)秀工作,如果對關于Smalltalk-80的MVC的更深入的歷史信息感興趣的話,我建議您讀一讀他的作品。

JavaScript 開發(fā)者可以使用的 MVC

我們已經(jīng)回顧了70年代,讓我們回到當下回到眼前?,F(xiàn)在,MVC模式已經(jīng)被應用到大范圍的編程語言當中,包括與我們關系最近的JavaScript。JavaScript領域現(xiàn)在有一些鼓勵支持MVC (或者是它的變種,我們稱之為MV* 家族)的框架,允許開發(fā)者不用付出太多的努力就可以往他們的應用中添加新的結構。

這些框架包括諸如Backbone, Ember.js和AngularJS??紤]到避免出現(xiàn)“意大利面條”式的代碼的重要性,該詞是指那些由于缺乏結構設計而導致難于閱讀和維護的代碼,對現(xiàn)代JavaScript開發(fā)者來說,了解該模式能夠提供什么已經(jīng)是勢在必行。這使得我們可以有效的領會到,這些框架能讓我們以不同的方式做哪些事情。

我們知道MVC由三個核心部分組成:

Models

Models管理一個業(yè)務應用的數(shù)據(jù)。它們既與用戶界面無關也與表現(xiàn)層無關,相反的它們代表了一個業(yè)務應用所需要的形式唯一的數(shù)據(jù)。當一個model改變時(比如當它被更新時),它通常會通知它的觀察者(比如我們很快會介紹的views)一個改變已經(jīng)發(fā)生了,以便觀察者采取相應的反應。

為了更深的理解models,讓我們假設我們有一個JavaScript的相冊應用。在一個相冊中,照片這個概念配得上擁有一個自己的model, 因為它代表了特定領域數(shù)據(jù)的一個獨特類型。這樣一個model可以包含一些相關的屬性,比如標題,圖片來源和額外的元數(shù)據(jù)。一張?zhí)囟ǖ恼掌梢源鎯Φ絤odel的一個實例中,而且一個model也可以被復用。下面我們可以看到一個用Backbone實現(xiàn)的被簡化的model例子。

var Photo = Backbone.Model.extend({

    // 照片的默認屬性
    defaults: {
      src: "placeholder.jpg",
      caption: "A default image",
      viewed: false
    },

    // 確保每一個被創(chuàng)建的照片都有一個`src`.
    initialize: function() {
       this.set( { "src": this.defaults.src} );
    }

});

不同的框架其內置的模型的能力有所不同,然而他們對于屬性驗證的支持還是相當普遍的,屬性展現(xiàn)了模型的特征,比如一個模型標識符.當在一個真實的世界使用模型的時候,我們一般也希望模型能夠持久.持久化允許我們用最近的狀態(tài)對模型進行編輯和更新,這一狀態(tài)會存儲在內存、用戶的本地數(shù)據(jù)存儲區(qū)或者一個同步的數(shù)據(jù)庫中。

另外,模型可能也會被多個視圖觀察著。如果說,我們的照片模型包含了一些元數(shù)據(jù),比如它的位置(經(jīng)緯度),照片中所展現(xiàn)的好友(一個標識符的列表)和一個標簽的列表,開發(fā)者也許會選擇為這三個方面的每一個提供一個單獨的視圖。

為現(xiàn)代MVC/MV*框架提供一種將模型組合到一起的方法(例如,在Backbone中,這些分組作為“集合”被引用)并不常見。管理分組中的模型允許我們基于來自分組中所包含的模型發(fā)生改變的通知,來編寫應用程序邏輯.這避免了手動設置去觀察每一個單獨的模型實體的必要。

如下是一個將模型分組成一個簡化的Backbone集合的示例:

var PhotoGallery = Backbone.Collection.extend({

    // Reference to this collection's model.
    model: Photo,

    // Filter down the list of all photos
    // that have been viewed
    viewed: function() {
        return this.filter(function( photo ){
           return photo.get( "viewed" );
        });
    },

    // Filter down the list to only photos that
    // have not yet been viewed
    unviewed: function() {
      return this.without.apply( this, this.viewed() );
    }
});

MVC上舊的文本可能也包含了模型管理著應用程序狀態(tài)的一種概念的引述.Javascript中的應用程序狀態(tài)有一種不同的意義,通常指的是當前的"狀態(tài)",即在一個固定點上的用戶屏幕上的視圖或者子視圖(帶有特定的數(shù)據(jù)).狀態(tài)是一個經(jīng)常被談論到的話題,看一看單頁面應用程序,其中的狀態(tài)的概念需要被模擬。

總而言之,模型主要關注的是業(yè)務數(shù)據(jù)。

視圖

視圖是模型的可視化表示,提供了一個當前狀態(tài)的經(jīng)過過濾的視圖。Smaltalk的視圖是關于繪制和操作位圖的,而JavaScript的視圖是關于構建和操作DOM元素的。

一個視圖通常是模型的觀察者,當模型改變的時候,視圖得到通知,因此使得視圖可以更新自身。用設計模式的語言可以稱視圖為“啞巴”,因為在應用程序中是它們關于模型和控制器的了解是受到限制的。

用戶可以和視圖進行交互,包括讀和編輯模型的能力(例如,獲取或者設置模型的屬性值)。因為視圖是表示層,我們通常以用戶友好的方式提供編輯和更新的能力。例如,在之前我們討論的照片庫應用中,模型編輯可以通過“編輯”視圖來進行,這個視圖里面,用戶可以選擇一個特定的圖片,接著編輯它的元數(shù)據(jù)。

而實際更新模型的任務落到了控制器上面(我們很快就會講這個東西)。

讓我們使用vanilla JavaScript 實現(xiàn)的例子來更深入的探索一下視圖。下面我們可以看到一個函數(shù)創(chuàng)建了一個照片視圖,使用了模型實例和控制器實例。

我們在視圖里定義了一個render()工具,使用一個JavaScript模板引擎來用于渲染照片模型的內容(Underscore的模板),并且更新了我們視圖的內容,供照片EI來參考。

照片模型接著將我們的render()函數(shù)作為一個其一個訂閱者的回調函數(shù),這樣通過觀察者模式,當模型發(fā)生改變的時候,我們就能觸發(fā)視圖的更新。

人們可能會問用戶交互如何在這里起作用的。當用戶點擊視圖中的任何元素,不是由視圖決定接下來怎么做。而是由控制器為視圖做決定。在我們的例子中,通過為photoEI增加一個事件監(jiān)聽器,來達到這個目的,photoEI將會代理處理送往控制器的點擊行為,在需要的時候將模型信息和事件一并傳遞。

這個架構的好處是每個組件在應用工作的時候都扮演著必要的獨立的角色。

var buildPhotoView = function ( photoModel, photoController ) {

  var base = document.createElement( "div" ),
      photoEl = document.createElement( "div" );

  base.appendChild(photoEl);

  var render = function () {
          // We use a templating library such as Underscore
          // templating which generates the HTML for our
          // photo entry
          photoEl.innerHTML = _.template( "#photoTemplate" , {
              src: photoModel.getSrc()
          });
      };

  photoModel.addSubscriber( render );

  photoEl.addEventListener( "click", function () {
    photoController.handleEvent( "click", photoModel );
  });

  var show = function () {
    photoEl.style.display = "";
  };

  var hide = function () {
    photoEl.style.display = "none";
  };

  return {
    showView: show,
    hideView: hide
  };

};

模板

在支持MVC/MV*的JavaScript框架的下,有必要簡略的討論一下JavaScript的模板以及它們與視圖之間的關系,在上一小節(jié),我們已經(jīng)接觸到這種關系了。

歷史已經(jīng)證明在內存中通過字符串拼接來構建大塊的HTML標記是一種糟糕的性能實踐。開發(fā)者這樣做,就會深受其害。遍歷數(shù)據(jù),將其封裝成嵌套的div,使用例如document.writeto 這樣過時的技術將"模板"注入到DOM中。這樣通常意味著校本化的標記將會嵌套在我們標準的標記中,很快就變得很難閱讀了,更重要的是,維護這樣的代碼將是一場災難,尤其是在構建大型應用的時候。

JavaScript 模板解決方案(例如Handlebars.js 和Mustache)通常用于為視圖定義模板作為標記(要么存儲在外部,要么存儲在腳本標簽里面,使用自定義的類型例如text/template),標記中包含有模板變量。變量可以使用變化的語法來分割(例如{{name}}),框架通常也足夠只能接受JSON格式的數(shù)據(jù)(模型可以轉化成JSOn格式),這樣我們只需要關心如何維護干凈的模型和干凈的模板。人們遭遇的絕大多數(shù)的苦差事都被框架本身所處理了。這樣做有大量的好處,尤其選擇是將模板存儲在外部的時候,這樣在構建大型引應用的時候可以是模板按照需要動態(tài)加載。

下面我們可以看到兩個HTMP模板的例子。一個使用流行的Handlebar.js框架實現(xiàn),一個使用Underscore模板實現(xiàn)。

Handlebars.js

<li class="photo">
  <h2>{{caption}}</h2>
  <img class="source" src="{{src}}"/>
  <div class="meta-data">
    {{metadata}}
  </div>
</li>

Underscore.js Microtemplates

<li class="photo">
  <h2><%= caption %></h2>
  <img class="source" src="<%= src %>"/>
  <div class="meta-data">
    <%= metadata %>
  </div>
</li><span style="line-height:1.5;font-family:'sans serif', tahoma, verdana, helvetica;font-size:10pt;"></span>

請注意模板并不是它們自身的視圖,來自于Struts Model 2 架構的開發(fā)者可能會感覺模板就是一個視圖,但并不是這樣的。視圖是一個觀察著模型的對象,并且讓可視的展現(xiàn)保持最新。模板也許是用一種聲明的方式指定部分甚至所有的視圖對象,因此它可能是從模板定制文檔生成的。

在經(jīng)典的web開發(fā)中,在單獨的視圖之間進行導航需要利用到頁面刷新,然而也并不值得這樣做。而在單頁面Javascript應用程序中,一旦數(shù)據(jù)通過ajax從服務器端獲取到了,并不需要任何這樣必要的刷新,就可以簡單的在同一個頁面渲染出一個新的視圖。

這里導航就降級為了“路由”的角色,用來輔助管理應用程序狀態(tài)(例如,允許用戶用書簽標記它們已經(jīng)瀏覽到的視圖)。然而,路由既不是MVC的一部分,也不在每一個類MVC框架中展現(xiàn)出來,在這一節(jié)中我將不深入詳細的討論它們。

總而言之,視圖是對我們的數(shù)據(jù)的一種可視化展現(xiàn)。

控制器

控制器是模型和視圖之間的中介,典型的職責是當用戶操作視圖的時候同步更新模型。

在我們的照片廊應用程序中,控制器會負責處理用戶通過對一個特定照片的視圖進行編輯所造成改變,當用戶完成編輯后,就更新一個特定的照片模型。

請記住滿足了MVC中的一種角色:針對視圖的策略模式的基礎設施。在策略模式方面,視圖在視圖的自由載量權方面代表了控制器。因此,那就是測試模式是如何工作的,視圖可以代表針對控制器的用戶事件,當視圖看起來合適的時候。視圖也可以代表針對控制器的模型變更事件處理,當視圖看起來合適的時候,但這并不是控制器的傳統(tǒng)角色。

大多數(shù)的Javascript MVC框架都受到了對"MVC"通常認知的影響,而這種認知是和控制器綁定在一起的.出現(xiàn)這種情況的原因各異,但在我的真實想法中,那是由于框架的作者一開始就將從服務器端的角度看待MVC,意識到它并不在客戶端進行1:1的翻譯,而對MVC中的C進行重新詮釋意在他們感覺更加有意義的事情.與此同在的問題在于它是主觀的,增加了理解經(jīng)典MVC模式的復雜度,當然還有控制器在現(xiàn)代框架中的角色。

作為示例,讓我們來簡要回顧一下當前流行的一種構造框架Backbone.js其架構.Backbone包含了模型和視圖(某些東西同我們前面看到的類似),然而它實際上并沒有真正的控制器.它的視圖和路由行為同控制器有一點點類似,但它們自身實際上都不是控制器。

在這一方面,同官方文檔或者博客文章中可能提到的相左,Backbone既不是一個真正的MVC/MVP框架,也不是一個MVVM框架.事實上把它看做是用它自身的方式架構方法的MV*家族中的一員,更加合適.當然這沒有任何錯誤的地方,但區(qū)分經(jīng)典MVC和MV*是重要的,我們應該依靠前者的經(jīng)典語法來幫助理解后者。

Spine.js VS Backbone.js

Spine.js

我們現(xiàn)在知道傳統(tǒng)的控制器負責當用戶更新視圖是同步更新模型.值得注意的一個有趣的地方是大多數(shù)時下流行的Javascript MVC/MV*框架在編寫的時候(Backbone)都沒有屬于它們自己的明確的控制器的概念。

因此,這對于我們從另一個MVC框架中體會到控制器實現(xiàn)的差異,并更進一步的展現(xiàn)出控制如何扮演著非傳統(tǒng)的角色是很有用處的.對于這一點,讓我們來看看來自于Spine.js的示例控制器。

在這個示例中,我們會有一個叫做PhotosController的控制器,用來管理應用程序中的個人照片.它將確保當視圖更新(例如,一個用戶編輯了照片的元數(shù)據(jù))時,對應的模型也會更新。

注意:我們并不會花大力氣研究Spine.js,而只是對它的控制器能做什么進行一定程度的了解:

// Controllers in Spine are created by inheriting from Spine.Controller

var PhotosController = Spine.Controller.sub({ 

  init: function () {
    this.item.bind( "update" , this.proxy( this.render ));
    this.item.bind( "destroy", this.proxy( this.remove ));
  },

  render: function () {
    // Handle templating
    this.replace( $( "#photoTemplate" ).tmpl( this.item ) );
    return this;
  },

  remove: function () {
    this.el.remove();
    this.release();
  }
});

在Spine中,控制器被認為是一個應用程序的粘合劑,對DOM事件進行添加和響應,渲染模板,還有確保視圖和模型保持同步(這在我們所知的控制器的上下文中起作用)。

我們在上面的example.js示例中所做的,是使用render()和remove()方法在更新和銷毀事件中設置偵聽器。當一個照片條目獲得更新的時候,我們對視圖進行重新渲染,以此反映對元數(shù)據(jù)的修改。類似的,如果照片從照片集中被刪除了,我們也會把它從視圖中移除。在render()函數(shù)中,我們使用Underscore微模板(通過_.template())來用ID #photoTemplate對一個Javascript模板進行渲染。這樣會簡單的返回一個編輯了的HTML字符串用來填充photoEL的內容。

這為我們提供了一個非常輕量級的,簡單的管理模型和視圖之間的變更的方法。

Backbone.js

后面的章節(jié)我們將會對Backbone和傳統(tǒng)MVC之間的區(qū)別進行一下重新審視,但現(xiàn)在還是讓我們專注于控制器吧。

在Backbone中,控制器的責任一分為二,由Backbone.View和Backbone.Router共享.前段時間Backbone確曾有其屬于自己的Backbone.Controller,但是對這一組件的命名對于它所被使用的上下文環(huán)境中并沒有什么意義,后來它就被重新命名為Router了。

Router比控制器要負擔處理著更多一點點的責任,因為它使得為模型綁定事件,以及讓我們的視圖對DOM事件和渲染產(chǎn)生響應,成為可能.如Tim Branyen(另外一名基于Bocoup的Backbone貢獻者)在以前所指出的,為此完全擺脫不使用Backbone.Router是有可能的,因此一種考慮讓它使用Router范式的做法可能像下面這樣:

var PhotoRouter = Backbone.Router.extend({
  routes: { "photos/:id": "route" },

  route: function( id ) {
    var item = photoCollection.get( id );
    var view = new PhotoView( { model: item } );

    $('.content').html( view.render().el );
  }
});

總之,本節(jié)的重點是控制器管理著應用程序中模型和視圖之間的邏輯和協(xié)作。

MVC給了我們什么?

MVC中關注分離的思想有利于對應用程序中功能進行更加簡單的模塊化,并且使得:

  • 整體的維護更加便利.當需要對應用程序進行更新時,到底這些改變是否是以數(shù)據(jù)為中心的,意味著對模型的修改還-有可能是控制器,或者僅僅是視覺的,意味著對視圖的修改,這一區(qū)分是非常清楚的。
  • 對模型和視圖的解耦意味著為業(yè)務邏輯編寫單元測試將會是更加直截了當?shù)摹?/li>
  • 對底層模型和控制器的代碼解耦(即我們可能會取代使用的)在整個應用程序中被淘汰了。
  • 依賴于應用程序的體積和角色的分離,這種模塊化允許負責核心邏輯的開發(fā)者和工作于用戶界面的開發(fā)者同時進行工作。

JavaScript中的Smalltalk-80 MVC

盡管當今主流的JavaScript框架都嘗試引入MVC的模式,來更好地面對web應用的開發(fā)。由Peter Michaux編寫的Maria.js ,是一個嘗試純正的Smalltalk-80的框架。其中,Model只是Model,View也只完成View應該做的,controller則只負責控制。然后,一些開發(fā)人員認為,MV*架構更值得關注,如果你對純正的MVC架構的JavaScript實現(xiàn)感興趣,這將是很好的參考。

更加深入的鉆研

在這本書的這一點上,我們應該對MVC模式提供了些什么有了一個基礎的了解,然而仍然有一些值得去關注的非常美妙的信息。

GoF并不將MVC引述為一種設計模式,而是把它看做是構建一個用戶界面的類的集合.按照他們的觀點,它實際上是三種經(jīng)典設計模式的變異組合:觀察者模式,策略模式和組件模式.依賴于框架中的MVC如何實現(xiàn),它也可能會使用工廠和模板模式.GoF Book提到這些模式在使用MVC工作時是非常有用的附加功能。

如我們所討論的,模型代表應用程序的數(shù)據(jù),而視圖則是用戶在屏幕上看到的被展現(xiàn)出來的東西.如此,MVC它的一些核心的通訊就要依賴于觀察者模式(令人驚奇的是,一些相關的內容在許多關于MVC模式的書籍并沒有被涵蓋到).當模型被改變時,它會通知觀察者(視圖)一些東西已經(jīng)被更新了——這也許是MVC中最重要的關系。觀察者的這一特性也是實現(xiàn)將多個視圖連結到同一個模型的基礎。

對于那些對MVC解耦特性想了解更多的開發(fā)者(這再一次依賴于特定的實現(xiàn)),這一模式的目標之一就是幫助去實現(xiàn)一個主體(數(shù)據(jù)對象)和它的觀察者之間的一對多關系的定義。當一個主體發(fā)生改變的時候,它的觀察者也會被更新。視圖和控制器有一種稍微不同的關系.控制器協(xié)助視圖對不同的用戶輸入做出響應,這也是一個策略模式的例子。

總結

回顧完經(jīng)典的MVC模式以后,我們現(xiàn)在應該理解了它是如何允許我們對一個應用程序中的各個關注點進行清晰地的區(qū)分.我們現(xiàn)在也應該感恩于Javascript MVC框架在它們對MVC模式的詮釋中是如何的不同,而其對變異也是相當開放的,仍然分享著其原生模式已經(jīng)提供的其中一些基礎概念。

當審視一個新的Javas MVC/MV*框架時,請記住——回過頭去考察考察它如何選擇相近的架構(特別的,它支持實現(xiàn)了模型,視圖,控制器或者其它的一些可選特性)可能會有些用處,因為這樣能夠更好的幫助我們深入了解這一框架預計需要被如何拿來使用。

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號