前端 MVC 變形記

背景:

MVC是一種架構設計模式,它經過關注點分離鼓勵改進應用程序組織。在過去,MVC被大量用於構建桌面和服務器端應用程序,現在Web應用程序的開 發已經愈來愈向傳統應用軟件開發靠攏,Web和應用之間的界限也進一步模糊。傳統編程語言中的設計模式也在慢慢地融入Web前端開發。因爲前端開發的環境 特性,在經典MVC模式上也引伸出了諸多MV*模式,被實現到各個Javascript框架中都有多少的衍變。在研究MV*模式和各框架的過程當中,倒是 「剪不斷、理還亂」:javascript

  1. 爲何每一個地方講的MVC都不太同樣?
  2. MVP、MVVM的出現是要解決什麼問題?
  3. 爲何有人義正言辭的說「MVC在Web前端開發中根本沒法使用」?

帶着十萬個爲何去翻閱不少資料,可是看起來像view、model、controller、解耦、監聽、通知、主動、被動、註冊、綁定、渲染等各 種術語的排列組合,像汪峯的歌詞似的。本篇但願用通俗易懂的方式闡述清楚一些關係,因爲接觸時間有限,英文閱讀能力有限,可能會存在誤解,歡迎討論和糾 正。html

MVC變形記

MVC歷史

MVC最初是在研究Smalltalk-80(1979年)期間設計出來的,恐怕沒有一本書可以回到計算機石器時代介紹一下Smalltalk的代 碼是如何實現MVC的,不只如此,連想搞清楚當時的應用場景都很難了,都要追溯到80後出生之前的事了。可是當時的圖形界面少之又少,施樂公司正在研發友 好的用戶圖形界面,以取代電腦屏幕上那些拒人於千里以外的命令行和DOS提示符。那時計算機世界天地混沌,渾然一體,而後出現了一個創世者,將現實世界抽 象出模型造成model,將人機交互從應用邏輯中分離造成view,而後就有了空氣、水、雞啊、蛋什麼的。在1995年出版的《設計模式:可複用面向對象 軟件的基礎》對MVC進行了深刻的闡述,在推廣使用方面發揮了重要做用。前端

MVC包括三類對象,將他們分離以提升靈活性和複用性。java

  • 模型model用於封裝與應用程序的業務邏輯相關的數據以及對數據的處理方法,會有一個或多個視圖監聽此模型。一旦模型的數據發生變化,模型將通知有關的視圖。linux

  • 視圖view是它在屏幕上的表示,描繪的是model的當前狀態。當模型的數據發生變化,視圖相應地獲得刷新本身的機會。git

  • 控制器controller定義用戶界面對用戶輸入的響應方式,起到不一樣層面間的組織做用,用於控制應用程序的流程,它處理用戶的行爲和數據model上的改變。angularjs

經典MVC模式

經典MVC模式github

實線:方法調用 虛線:事件通知算法

其中涉及兩種設計模式:編程

  • view和model之間的觀察者模式,view觀察model,事先在此model上註冊,以便view能夠了解在數據model上發生的改變。

  • view和controller之間的策略模式

一個策略是一個表述算法的對象,MVC容許在不改變視圖外觀的狀況下改變視圖對用戶輸入的響應方式。例如,你可能但願改變視圖對鍵盤的響應方式,或 但願使用彈出菜單而不是原來的命令鍵方式。MVC將響應機制封裝在controller對象中。存在着一個controller的類層次結構,使得能夠方 便地對原有的controller作適當改變而建立新的controller。

view使用controller子類的實例來實現一個特定的響應策略。要實現不一樣的響應的策略只要用不一樣種類的controller實例替換即 可。甚至能夠在運行時刻經過改變view的controller來改變用戶輸入的響應方式。例如,一個view能夠被禁止接受任何輸入,只需給他一個忽略 輸入事件的controller。

好吧,若是被上述言論繞昏了,請繼續研讀《設計模式:可複用面向對象軟件的基礎》。

MVC for JAVASCRIPT

咱們回顧了經典的MVC,接下來說到的MVC主要是在Javascript上的實現。

javascript MVC模式

javascript MVC模式

源圖

如圖所示,view承接了部分controller的功能,負責處理用戶輸入,可是沒必要了解下一步作什麼。它依賴於一個controller爲她作 決定或處理用戶事件。事實上,前端的view已經具有了獨立處理用戶事件的能力,若是每一個事件都要流經controller,勢必增長複雜性。同 時,view也能夠委託controller處理model的更改。model數據變化後通知view進行更新,顯示給用戶。這個過程是一個圓,一個循環 的過程。

這種從經典MVC到Javascript MVC的1對1轉化,致使控制器的角色有點尷尬。MVC這樣的結構的正確性在於,任何界面都須要面對一個用戶,而controller 「是用戶和系統之間的連接」。在經典MVC中,controller要作的事情多數是派發用戶輸入給不一樣的view,而且在必要的時候從view中獲取用 戶輸入來更改model,而Web以及絕大多數如今的UI系統中,controller的職責已經被系統實現了。因爲某種緣由,控制器和視圖的分界線越來 越模糊,也有認爲,view啓動了action理論上應該把view歸屬於controller。好比在Backbone中,Backbone.View 和Backbone.Router一塊兒承擔了controller的責任。這就爲MVC中controller的衍變埋下了伏筆。

MVP

MVP(model-view-Presenter)是經典MVC設計模式的一種衍生模式,是在1990年代Taligent公司創造的,一個用於C++ CommonPoint的模型。背景上再也不考證,直接上圖看一下與MVC的不一樣。

MVP模式

MVP模式

經典MVC中,一對controller-view捆綁起來表示一個ui組件,controller直接接受用戶輸入,並將輸入轉爲相應命令來調用model的接口,對model的狀態進行修改,最後經過觀察者模式對view進行從新渲染。

進化爲MVP的切入點是修改controller-view的捆綁關係,爲了解決controller-view的捆綁關係,將進行改造,使 view不只擁有UI組件的結構,還擁有處理用戶事件的能力,這樣就能將controller獨立出來。爲了對用戶事件進行統一管理,view只負責將用 戶產生的事件傳遞給controller,由controller來統一處理,這樣的好處是多個view可共用同一個controller。此時的 controller也由組件級別上升到了應用級別,然而更新view的方式仍然與經典MVC同樣:經過Presenter更新model,經過觀察者模 式更新view。

另外一個顯而易見的不一樣在於,MVC是一個圓,一個循環的過程,但MVP不是,依賴Presenter做爲核心,負責從model中拿數據,填充到 view中。常見的MVP的實現是被動視圖(passive view),Presenter觀察model,再也不是view觀察model,一旦model發生變化,就會更新view。Presenter有效地綁 定了model到view。view暴露了setters接口以便Presenter能夠設置數據。對於這種被動視圖的結構,沒有直接數據綁定的概念。但 是他的好處是在view和model直接提供更清晰的分離。可是因爲缺少數據綁定支持,意味着不得不單獨關注某個任務。在MVP裏,應用程序的邏輯主要在 Presenter來實現,其中的view是很薄的一層。

MVVM

MVVM,Model-View-ViewModel,最初是由微軟在使用Windows Presentation Foundation和SilverLight時定義的,2005年John Grossman在一篇關於Avalon(WPF 的代號)的博客文章中正式宣佈了它的存在。若是你用過Visual Studio, 新建一個WPF Application,而後在「設計」中拖進去一個控件、雙擊後在「代碼」中寫事件處理函數、或者綁定數據源。就對這個MVVM有點感受了。好比VS自 動生成的以下代碼:

<GroupBox Header="綁定對象">
    <StackPanel Orientation="Horizontal" Name="stackPanel1">
        <TextBlock Text="學號:"/>
        <TextBlock Text="{Binding Path=StudentID}"/>
        <TextBlock Text="姓名:"/>
        <TextBlock Text="{Binding Path=Name}"/>
        <TextBlock Text="入學日期:"/>
        <TextBlock Text="{Binding Path=EntryDate, StringFormat=yyyy-MM-dd}"/>
        <TextBlock Text="學分:"/>
        <TextBlock Text="{Binding Path=Credit}"/>
    </StackPanel>
</GroupBox>
stackPanel1.DataContext = new Student() {
StudentID=20130501,
Name="張三",
EntryDate=DateTime.Parse("2013-09-01"),
Credit=0.0
};

其中最重要的特性之一就是數據綁定,Data-binding。沒有先後端分離,一個開發人員全搞定,一隻手抓業務邏輯、一隻手抓數據訪問,順帶手拖放幾個UI控件,綁定數據源到某個對象或某張表,一步到位。

背景介紹完畢,再來看一下理論圖

MVVM模式

MVVM模式

首先,view和model是不知道彼此存在的,同MVP同樣,將view和model清晰地分離開來。 其次,view是對viewmodel的外在顯示,與viewmodel保持同步,viewmodel對象能夠看做是view的上下文。view綁定到viewmodel的屬性上,若是viewmodel中的屬性值變化了,這些新值經過數據綁定會自動傳遞給view。反過來viewmodel會暴露model中的數據和特定狀態給view。 因此,view不知道model的存在,viewmodel和model也覺察不到view。事實上,model也徹底忽略viewmodel和view的存在。這是一個很是鬆散耦合的設計。

流行的MV*框架:

每一個框架都有本身的特性,這裏主要討論MVC三個角色的責任。粗淺地過一遍每一個框架的代碼結構和風格。

BackboneJS

Backbone經過提供模型Model、集合Collection、視圖View賦予了Web應用程序分層結構,其中模型包含領域數據和自定義事件;集合Colection是模型的有序或無序集合,帶有豐富的可枚舉API; 視圖能夠聲明事件處理函數。最終將模型、集合、視圖與服務端的RESTful JSON接口鏈接。

Backbone在升級的過程當中,去掉了controller,由view和router代替controller,view集中處理了用戶事件(如click,keypress等)、渲染HTML模板、與模型數據的交互。Backbone的model沒有與UI視圖數據綁定,而是須要在view中自行操做DOM來更新或讀取UI數據。Router爲客戶端路由提供了許多方法,並能鏈接到指定的動做(actions)和事件(events)。

Backbone是一個小巧靈活的庫,只是幫你實現一個MVC模式的框架,更多的還須要本身去實現。適合有必定Web基礎,喜歡原生JS去操做DOM(由於沒有數據綁定)的開發人員。爲何稱它爲庫,而不是框架,不只僅是因爲僅4KB的代碼,更重要的是 使用一個庫,你有控制權。若是用一個框架,控制權就反轉了,變成框架在控制你。庫可以給予靈活和自由,可是框架強制使用某種方式,減小重複代碼。這即是Backbone與Angular的區別之一了。

至於Backbone屬於MV*中的哪一種模式,有人認爲不是MVC,有人以爲更接近於MVP,事實上,它借用多個架構模式中一些很好的概念,建立一個運行良好的靈活框架。沒必要拘泥於某種模式。

// view:
var Appview = Backbone.View.extend({
// 每一個view都須要一個指向DOM元素的引用,就像ER中的main屬性。
el: '#container',

// view中不包含html標記,有一個連接到模板的引用。
template: _.template("<h3>Hello <%= who %></h3>"),

// 初始化方法
initialize: function(){
this.render();
},

// $el是一個已經緩存的jQuery對象
render: function(){
this.$el.html("Hello World");
},

// 事件綁定
events: {'keypress #new-todo': 'createTodoOnEnter'}
});
var appview = new Appview();

// model:
// 每一個應用程序的核心、包含了交互數據和邏輯
// 如數據驗證、getter、setter、默認值、數據初始化、數據轉換
var app = {};

app.Todo = Backbone.model.extend({
defaults: {
title: '',
completed: false
}
});

// 建立一個model實例
var todo = new app.Todo({title: 'Learn Backbone.js', completed: false});
todo.get('title'); // "Learn Backbone.js"
todo.get('completed'); // false
todo.get('created_at'); // undefined
todo.set('created_at', Date());
todo.get('created_at'); // "Wed Sep 12 2012 12:51:17 GMT-0400 (EDT)"

// collection:
// model的有序集合,能夠設置或獲取model
// 監聽集合中的數據變化,從後端獲取模型數據、持久化。
app.TodoList = Backbone.Collection.extend({
model: app.Todo,
localStorage: new Store("backbone-todo")
});

// collection實例
var todoList = new app.TodoList()
todoList.create({title: 'Learn Backbone\'s Collection'});

// model實例
var model = new app.Todo({title: 'Learn models', completed: true});
todoList.add(model);
todoList.pluck('title');
todoList.pluck('completed');

KnockoutJS

KnockoutJS是一個名正言順的MVVM框架,經過簡潔易讀的data-bind語法,將DOM元素與viewmodel關聯起來。當模型(viewmodel)狀態更新時,自動更新UI界面。 viewmodel是model和view上的操做的一個鏈接,是一個純粹的Javascript對象。它不是UI,沒有控件和樣式的概念,它也不是持久化的模型數據,它只是hold住一些用戶正在編輯的數據,而後暴露出操做這些數據(增長或刪除)的方法。

view是對viewmodel中數據的一個可視化的顯示,view觀察viewmodel,操做view時會發送命令到viewmodel,而且當viewmodel變化時更新。view和model是不瞭解彼此的存在的。

<form data-bind="submit: addItem">
    New item:
    <input data-bind='value: itemToAdd, valueUpdate: "afterkeydown"' />
    <button type="submit" data-bind="enable: itemToAdd().length > 0">Add</button>
    <p>Your items:</p>
    <select multiple="multiple" width="50" data-bind="options: items"> </select>
</form>
// viewmodel
var SimpleListmodel = function(items) {
    this.items = ko.observableArray(items);
    this.itemToAdd = ko.observable("");
    this.addItem = function() {
        if (this.itemToAdd() != "") {
// 把input中的值加入到items,會自動更新select控件
            this.items.push(this.itemToAdd());
// 清空input中的值
            this.itemToAdd("");
        }
// 確保這裏的this一直是viewmodel
    }.bind(this);
};

ko.applyBindings(new SimpleListmodel(["Alpha", "Beta", "Gamma"]));

AngularJS

AngularJS試圖成爲Web應用中的一種端對端的解決方案。這意味着它不僅是你的Web應用中的一個小部分,而是一個完整的端對端的解決方案。這會讓AngularJS在構建一個CRUD的應用時看起來很呆板,缺少靈活性。AngularJS是爲了克服HTML在構建應用上的不足而設計的。使用了不一樣的方法,它嘗試去補足HTML自己在構建應用方面的缺陷。經過使用標識符(directives)的結構,讓瀏覽器可以識別新的語法。例如使用雙大括號語法進行數據綁定;使用ng-controller指定每一個控制器負責監視視圖中的哪一部分;使用ng-model,把輸入數據綁定到模型中的一部分屬性上。

雙向數據綁定是AngularJS的另外一個特性。UI控件的任何更改會當即反映到模型變量(一個方向),模型變量的任何更改都會當即反映到問候語文本中(另外一方向)。AngularJS經過做用域來保持數據模型與視圖界面UI的雙向同步。一旦模型狀態發生改變,AngularJS會當即刷新反映在視圖界面中,反之亦然。

AngularJS本來是傾向於MVC,可是隨着項目重構和版本升級,如今更接近MVVM。和Knockout view中的風格相似,都像從WPF衍變過來的,只是Knockout使用了自定義屬性data-bind做爲綁定入口,而AngularJS對於HTML的變革更完全,擴展HTML的語法,引入一系列的指令。

在AngularJS中,一個視圖是模型經過HTML模板渲染以後的映射。這意味着,不論模型何時發生變化,AngularJS會實時更新結合點,隨之更新視圖。好比,視圖組件被AngularJS用下面這個模板構建出來:

<body ng-controller="PhoneListCtrl">
<ul>
<li ng-repeat="phone in phones">
{{phone.name}}
<p>{{phone.snippet}}</p>
</li>
</ul>
</body>

在li標籤裏面的ng-repeat語句是一個AngularJS迭代器。包裹在phone.name和phone.snippet周圍的花括號標識着數據綁定,是對應用一個數據模型的引用。當頁面加載的時候,AngularJS會根據模版中的屬性值,將其與數據模型中相同名字的變量綁定在一塊兒,以確保二者的同步性。

在PhoneListCtrl控制器裏面初始化了數據模型:

// controller:
function PhoneListCtrl($scope) {
// 數組中存儲的對象是手機數據列表
$scope.phones = [
{"name": "Nexus S",
"snippet": "Fast just got faster with Nexus S."},
{"name": "Motorola XOOM™ with Wi-Fi",
"snippet": "The Next, Next Generation tablet."},
{"name": "MOTOROLA XOOM™",
"snippet": "The Next, Next Generation tablet."}
];
}

儘管控制器看起來並無什麼控制的做用,可是它在這裏的重要性在於,經過給定數據模型的做用域$scope,容許創建模型和視圖之間的數據綁定。方法名PhoneListCtrl和body標籤裏面的ngcontroller指令的值相匹配。當應用啓動以後,會有一個根做用域被建立出來,而控制器的做用域是根做用域的一個典型後繼。這個控制器的做用域對全部

標記內部的數據綁定有效。

AngularJS的做用域理論很是重要:一個做用域能夠視做模板、模型和控制器協同工做的粘接器。AngularJS使用做用域,同時還有模板中的信息,數據模型和控制器。這些能夠幫助模型和視圖分離,可是他們二者確實是同步的!任何對於模型的更改都會即時反映在視圖上;任何在視圖上的更改都會被馬上體如今模型中。

實踐中的思考

咱們使用的MVC框架是ER,適用於並能很方便地構建一個整站式的AJAX Web應用。提供精簡、核心的action、model和view的抽象,使得構建RIA應用變得簡單可行。在使用的過程當中近距離地體會到很是多方面的優秀的設計理念。也讓我開始思考各個角色的轉型。

讓view上前線

我開始思考action(controller)這個角色。我以爲從純粹地解耦角度來講,view和model應該是互相不知道彼此存在的,全部的事件流和對數據、UI的處理應該都流經action。可是這一點又極不現實。用戶操做了一個UI,須要更新model的一個數據,就要fire到action,經過action來調用model的set方法。這樣又有點麻煩,由於view中有對model的應用,能夠一句代碼搞定這一個數據的設置。因此,我本身設置了一個規則:若是是簡單的模型數據讀寫能夠直接在view中操做;若是要通過複雜的數據處理,必須流經action。因而,我遇到了一種怎麼都偷不了懶(必須通過action)的狀況: 好比有個主action main,兩個子action list、select,用戶在list中的view選擇一條數據添加到右側select中。那走過的流程是這樣的:

實踐中的思考

實踐中的思考

  1. 子Action中的listView接受UI事件,fire到listAction中
  2. listAction繼續將事件fire到mainView中,由主action來處理另外子Action的事情。
  3. mainView接收到事件、調用子Action selectAction的方法
  4. selectAction繼續調用selectView的方法來完成UI的更新。

其中涉及的model的變化暫時不考慮。我在想,view既然把經典MVC中的controller接受用戶事件的角色承接過來的,那若是借鑑Backbone的思想,把view做爲controller的一個實現,推到戰場的最前線。省掉兩次action的中轉傳遞,是否是更簡單。

model驅動開發

實際開發中,經常會以view爲核心,頁面上須要展現什麼數據,就去model中設置數據源。發生了用戶事件,我會在action中更新model,而後刷新view。有時候會遺漏更新model,直到須要數據時才發現沒有保存到model中。

model自己是獨立的,自控制的,不依賴於view,可以同步支持多view的顯示。就像linux上的應用程序一般會提供圖形界面和命令行兩種操做方式同樣。那若是以model爲核心,model驅動開發,數據在手、天下我有,以模型驗證保證數據的完整性和正確性。實現數據綁定,任何對模型的更改都會在界面上反映出來。那咱們只要預先寫好view和model的關係映射(相似viewmodel),而後只關注模型數據,就OK了。

對於MV*家族,都是在經典MVC基礎上隨着時代的發展、應用環境的變化衍變出來的。實現MV*模式的這些框架到底歸屬於哪一種模式,也沒必要泥古。MV*是一個頗有爭議性的話題,可以構建一個健壯、具備良好設計、聽從關注點分離的項目比花時間去爭論究竟是MV*更有意義。

相關文章
相關標籤/搜索