1. 程式人生 > >前端架構 MVC VS Flux

前端架構 MVC VS Flux

前言

在學習React.js的過程中,曾經最讓我苦惱的事情是,我需要給自己一個使用這個框架的理由。因為隨著學習經驗的和工作經驗的增長,你會發現類似的技術總是會此消彼長的出現,如果這只是另一個輪子怎麼辦?加之學習的成本、專案改造的成本甚至周圍人來適應你的成本,一味的追逐最新最流行的技術並非是一件好事。

當談React.js時有必要把它一分為二來講解。首先要明確的是它只是一個用於檢視層的類庫(lib),你能把它當作模板引擎來用( React.js並不是模板引擎 ),輸出html,定義元件。但是你沒法僅用React.js類庫就搭建一個完整的前端app。之所以要強調這麼一點是因為很多人會拿React.js與Angular做比較,這是不公平的,因為angular的定位是一個框架(framework)。angular自帶模板引擎,路由引擎,有健全的資料雙向繫結機制,內建Ajax請求功能,還能夠定義Model。而React.js類庫只能用於定義檢視元件。

React.js的另一面是它背後的flux架構,這是我們這篇文章著重要談的。但正如上一段所說,React.js僅依靠自己沒法成型一個flux框架,基於React.js的flux框架要麼是手動補全了框架中其他角色的程式碼,要麼引入了其他的第三方類庫。flux架構也並非是React.js的專利,市面上已經有非常多的獨立於React.js的開源框架供使用。

這篇文章的目的就是讓你讀懂flux架構,我們會直接把flux與mvc比較,來彰顯它的優劣。flux架構並非新事物,如果你擁有後端開發背景的話,flux架構一定會讓你聯想到CQRS(Command-Query Responsibility Segregation)、EDA(Event-Driven Architecture)、DDD(Domain-Driven Design)等概念。至於這些概念具體是什麼,和flux有什麼關係,會在下一篇中介紹。今天我們聊flux與mvc比較之下它的創新之處在哪。

如果你還完全沒有接觸過React.js也不太要緊。這一篇的內容主要集中於用圖解和文字來講解架構之間的差異,程式碼部分簡單通俗易懂。

MVC簡介

MVC架構講程式劃分為三個角色,從上到下依次為: - View: 檢視,使用者資料展示,同時接受使用者輸入 - Contorller:響應使用者的輸入,對資料進行操作, - Model:負責管理程式需要的資料,並且定義了操作資料的行為。

對於一個簡單的MVC架構程式來說,其工作流程如下:

從最右邊的View開始,當用戶在UI上進行操作之後,使用者的操作被轉發到了Controller上,Controller根據使用者的操作對資料進行更新(準確來說是呼叫Model層的API),資料更新之後自然檢視View展現的內容也需要進行更新。Model層此時可以向所有關聯的檢視發出通知,收到通知的檢視重新獲取最新的資料。注意這最後一步Model與View的互動,大部分現有的MVC框架將其進行了封裝,開發人員只要使用資料繫結即可。

如果上面的流程圖還過於抽象的話,我們可以看一段MVC專案的程式碼,比如基於Nodejs的Kraken框架的 Shopping_Cart 示例專案中的controller controllers/index.js :

var Product = require('../models/productModel');

module.exports = function(server){
    server.get('/', function(req,res){
        Product.find(function(err,prods){
            if (err) {
                console.log(err);
            }

            var model =
            {
                products: prods
            };

            res.render('index', model);
        });
    });
};

由於這是一個後端框架,使用者的操作只能通過url路徑體現。當用戶訪問 / 路徑時,首頁 index.html 對應的controller,也就是該 controllers/index.js 收到請求,它呼叫Model層的Product模組的 find 方法請求資料,並將或得到的資料交給 index 模板進行重新渲染,產生的頁面返回給使用者。

為了和flux做比較,在這裡我們要強調幾點:

  • 通常View和Controller的關係是一一對應的,比如首頁index.html有自己的controller controllers/index.js ,查詢頁面search.html有自己的controller controllers/search.js 。從下面這段angular的路由程式碼 就是很典型的示例:
phonecatApp.config(['$routeProvider',
  	function($routeProvider) {
    	$routeProvider.
      		when('/phones', {
        		templateUrl: 'partials/phone-list.html',
        		controller: 'PhoneListCtrl'
      		}).
      		when('/phones/:phoneId', {
        		templateUrl: 'partials/phone-detail.html',
        		controller: 'PhoneDetailCtrl'
      		}).
      		otherwise({
        		redirectTo: '/phones'
      		});
}]);
  • controller是有業務邏輯的。雖然在MVC中我們強調”fat model, skinny controller”(業務邏輯應儘量放在Model層,Controller只應該作為View與Model的介面),但skinny並不代表none,controller中還是有與業務相關的邏輯來決定將如何轉發使用者的請求,最典型的決定是轉發到哪個Model層。

  • Model應該被更準確的稱為Domain Model(領域模型),它不代表具體的Class或者Object,也不是單純的databse。而是一個“層”的概念:資料在Model裡得到儲存,Model提供方法操作資料(Model的行為)。所以Model程式碼可以有業務邏輯,甚至可以有資料的儲存操作的底層服務程式碼。

  • MVC中的資料流是雙向的,模型通知檢視資料已經更新,檢視直接查詢模型中的資料。

MVC的侷限

上小節單組MVC(View、Model、Controller是1:1:1的關係)只是一種理想狀態。現實中的程式往往是 多檢視 , 多模型 。更嚴重的是檢視與模型之間還可以是 多對多 的關係。也就是說,單個檢視的資料可以來自多個模型,單個模型更新是需要通知多個檢視,使用者在檢視上的操作可以對多個模型造成影響。可以想象最致命的後果是,檢視與模型之間相互更新的死迴圈。

這樣一來,View與Model與Controller之間的關係就成一團亂麻了,如下兩幅圖所示:

如此的混亂會產生很多的問題,比如除錯程式碼。假設在一個複雜的MVC的架構中,有多個controller可以修改model,而開發時model的資料產出並非如你所願,則你很難判斷出是哪個controller出的錯,只能使用控制變數法進行除錯。

在2014年Facebook舉辦的F8(Facebook Developer Conference)大會上其中的 Hacker Way: Rethinking Web App Development at Facebook 單元裡,Facebook的工程師Jing Chen對於MVC的評價是,MVC非常適合於小型應用,但是當許許多多的Model和與之對應的View被加入到一個系統中,情況就會變得如下圖所示:

需要注意的是,她想表達的意思其實和上述兩幅圖是相同的,但她在大會上演示的這幅圖對MVC的架構描述是有欠缺的。她的這番言論和不準確的圖片同時也在 Reddit上也引起了非常多的討論 ,甚至是負面的評價。最後她的回覆如下

Yeah, that was a tricky slide [the one with multiple models and views and bidirectional data flow], partly because there’s not a lot of consensus for what MVC is exactly - lots of people have different ideas about what it is. What we’re really arguing against is bi-directional data flow, where one change can loop back and have cascading effects.

她承認演示中的圖片確實投機取巧了。但其實大部分人對MVC的見解也並不相同,它們真正想表達的是這種雙向的資料流架構會產生一定的負面效應。

Flux

一個簡單的flux流程圖如下所示:

參照上面的圖示,我們首先總結一下,flux架構下一共有四類模組角色,按照互動順序依次是:

  • Component/View: 你可以把元件(Component)理解為View與Controller的結合,它既展現資料,同時也處理使用者的互動請求。不同於MVC的Controller直接呼叫模型層業務邏輯處理介面,flux上來自使用者的操作或者請求最終會對映為對應的Action,交由Action進行下一步處理。另一點需要注意的是View同時也監聽著Store中資料的更改事件,一旦發生更改則重新請求資料。

  • Action:描述元件觸發的操作,包括名稱和資料,比如 { 'actionType': 'delete', 'data':item}

  • Dispatcher: flux的中央樞紐(central hub),所有的Action都會交由Dispatcher進行處理。Dispatcher在接收到Action之後,呼叫Store註冊在Action上的回撥函式。需要注意與MVC中Controller不同的是,Dispatcher是不包含業務邏輯的,它機械的像一座橋,一個路由器,所以它能被別的程式複用當然也能被別的Dispatcher替換。

  • Store:包含程式的資料與業務邏輯。和MVC的Model比較,Store有一些不易被察覺但又非常重要的差異:MVC中的每一個model即對應著一個領域模型;而flux中的一個Store自己並不是一個領域模型,而是可能包含多個模型。 最重要的是 ,只有store自己知道如何修改資料,它並不對外直接提供操作資料的介面(但是提供查詢資料的介面),action和dispatcher沒法操作store.

一個簡單的flux流程我們可以這麼描述:使用者在View上的操作最終會對映為一類Action,Action傳遞給Dispatcher,再由Dispatcher執行註冊在指定Action上的回撥函式。最終完成對Store的操作。如果Store中的資料發生了更改,則觸發資料更改的事件,View監聽著這些時間,並對這些事件做出反應(比如重新查詢資料)。

當有多個Store和View被新增後,複雜的flux流程圖如下圖所示

如果上圖還是讓你感覺到複雜的話,我們繼續抽象flux流程如下:

由此可見即使是複雜的flux應用,它的資料流和程式的運作過程仍然是清晰可辨的。

Flux程式碼

最後這一小節,是用程式碼來演示flux的簡易實現。如果你閱讀本文的目的只是想對flux原理稍加了解,則可以略過這小節內容。

View

我們從最簡單的場景出發,假設頁面上只有一個按鈕,我們通過這個按鈕向store裡新增一條資料。這裡檢視我們通過Reactjs實現:

var View = React.createClass({  
    addNewItem: function(event){
        Dispatcher.dispatch({
          action: 'add_item',
          data: {date: +new Date}        
        });
    },
    render: function(){
        return (
            <buttononClick={this.addNewItem}>AddItem</button>
        )
    }
});

在按鈕的點選事件中我們觸發了 add_item 事件。只不過觸發事件是直接通過呼叫 Dispatcher 來實現。

Actions

在上面的檢視程式碼中,我們直接呼叫了Dispatcher的方法。但這樣的程式碼耦合太強了。View其實無需感知Dispatcher,這裡我們更是直接把Dispatcher的細節暴露給了View,同時action也沒有被抽象出來。

接下來我們把Action抽象出來

var Actions = {
  add: function(item){
      Dispatcher.dispatch({
          action: 'add_item',
          data: item        
      });   
  }
}

此時的View也要修改為:

var View = React.createClass({  
    addNewItem: function(event){
        Actions.add({
            date: +new Date
        });
    },
    render: function(){
        return (
            <buttononClick={this.addNewItem}>AddItem</button>
        )
    }
});

Store

Store負責儲存並更新資料,它需要監聽Dispatcher上觸發的action並做出響應:

var Store = {
    items: []
}

Dispatcher.register(function(payload) {
    switch(payload.action) {
        case 'add_item':
            // 當事件名為“新增”時,向倉庫裡新增資料
            Store.items.push(payload.data);
            // 同時觸發“資料已更改”的事件
            Store.triggerEvent('change');
            break;
    }
}); 

當Store更新完資料之後,它還需要觸發一個數據更新的事件,以告知那些關注這些資料的人。如果我們的檢視需要在資料更改後時時更新資料,則還需要在Store註冊資料更改事件的回撥函式

var View = React.createClass({
    update: function(){
        // TODO
    }, 
    componentDidMount: function(){  
        Store.bind('change', this.update);
    },  
    addNewItem: function(event){
        Actions.add({
            date: +new Date
        });
    },
    render: function(){
        return (
            <buttononClick={this.addNewItem}>AddItem</button>
        )
    }
});