前端架構101(四):MVC的不足與Flux的崛起

NO IMAGE

MVC 的不足

事件

在前幾篇中,我演示了一個前端 Backbone.js MVC 框架用於解決實際問題的例子。但 MVC 依然存在幾個問題

  • 不可預測:當一個事件發生之後,你並不知道會有誰響應這個事件,是單個對象還是多個對象會響應這個事件
  • 級聯修改:當一個事件發生之後,A 組件在接收到事件之後在響應的過程中,還可能發出其他的事件觸發後續的修改,你並不知道這個事件會在何處結束,會造成什麼樣的結果。這也和上一條「不可預測」相對應
  • 響應順序:如果存在多個對象響應同一個事件的話,有時候對響應的順序是有要求的,某些變更不可以出現在其他的變更之前
  • 有條件響應:對於傳播方而言,並非希望所有的時間都一視同仁的廣播出去;對於消費方而言,也並不希望一視同仁的響應所有的事件

你可能會認為事件機制存在的問題是否只存在於 Backbone.js 中,那 AngularJS 這個 MVC 框架會不會好一些呢?

首先 AngularJS(AngularJS 代指 1.x 版本,Angular 代指 2 以及之後的版本) 框架中也支持全局的事件機制,比如 $broadcast,$emit 等等。這樣的事件機制支持變化從 $rootScope 向各個 contoller 的 $scope 廣播全局的變化。如果你對 scope 這個概念不熟悉的話,可以把它理解為模塊內部的作用域。

前端架構101(四):MVC的不足與Flux的崛起

雙向綁定

AngularJS 更重大的缺陷在於它的雙向綁定機制,或者說是雙向數據流 (bidirection data flow) 。也就是說 A 可以把變量傳遞給 B,當 B 修改這個變量之後,A 中對應的變量值也會發生修改。咋聽之下似乎是非常方便的機制,例如在表單這個場景中會非常實用,但是它存在一些隱患。我們以下圖中的這個場景為例:

前端架構101(四):MVC的不足與Flux的崛起

  1. Parent Controller 把某個變量以雙向綁定的機制傳遞給 Child A Controller

  2. 此時用戶在界面上對這個變量值進行了修改

  3. 因為雙向綁定的緣故這個值同步到了 Parent Controller 中

  4. 同時 Child B Contoller 和 Parent Controller 也通過雙向綁定把值同步到了 Child B 中,此時 Child B 中的值也發生了修改

也就是說,當你修改 Child A 中的一個值時,你會影響到 Child B 中的值。這樣的副作用是危險的,除非你對整個系統裡用到這個值的地方瞭如指掌,否則你極有可能影響到你不願意被你影響到的地方。

如果 Child A 和 Child B 屬於不同的開發人員進行開發, 那麼 Child B 的開發人員在排查這個問題是會非常困難,因為站在他的視角上而言,他只知道這個值來自於 Parent Controller,但是這個值又被哪些地方消費了,哪些地方修改了他並不知道。在框架機制內不支持這樣的追溯。此時你只能保佑關於這個變量有一個 setter 方法,又或者通過 IDE 的查找功能在代碼裡全局搜索用到這個變量的地方

職責不明確

回憶一下我在第二篇中列舉的 Backbone.js 和 AngularJS 實現的例子,無論是 view 文件還是 controller 文件,其它們的職責並不明確,他們同時在負責好幾件事情:

  • 管理 view model,例如負責保存和清空用戶輸入的值
  • 協調用戶流程,例如首先將用戶輸入值清空,然後提交新數據,再刷新數據列表
  • 負責為不同的 dom 元素綁定事件處理函數

不說大道理,和當下的 React 或者 Angular 組件相比,直接後果是這些模塊是無法複用的。如果我想重複使用一個 view 的話,我需要保證我的頁面模版裡有相同的 id 的元素,又必須保證上下文中有相同 model 層提供相同的藉口或者廣播相同的事件。關於多職責的壞處在上一篇中已經聊過,就不贅述了。

總結

批評不等於否定。事件機制依然是我們許多問題裡可選的解決方案之一;Backbone.js 和 AngularJS 放在現在看也依然是優秀的解決框架,但不是最優解而已。

我個人認為問題在於當下我們解決的問題和過去比發生了許多的變化,隨著瀏覽器能力不斷增強,前端需要解決的問題也變得越來越複雜,團隊規模也逐漸擴大。如果以 React 步入公眾視野的 2014 為節點的話(我以)。2014 年以前我們的開發主要集中在類似於 widget / plugin 級別的功能上;而在 2014 年之後應用級別的功能慢慢變得普及起來。

如果你對比 2014 年以後和之後流行或者崛起的那些框架,你就會感受到其中的微妙之處:

  • 2014 年前:jQuery, Bootstrap, RequireJS, Kissy, Handlebars
  • 2014 年後:Redux, Ngrx, Mobx, Akita, Ngxs

前者傾向於碎片化,各司其職的輔助性的功能;後者傾向於應用級別的數據管理

事件機制和雙向綁定更適用於小規模的範圍內,隨著應用級別不斷擴大,副作用的帶來負面效用會變得越來越明顯。

Flux

我把所有與 Flux 相似的框架在這裡都稱之為 Flux。包括但不限於:Redux,Mobx,Ngrx,Akita,React 等等。在我看來它們都擁有和 Flux 相同的特徵:

  • 單向數據流
  • 全局狀態管理
  • store / selector / service 等概念的抽象

在談論 Flux 之前我們先給 Flux 定一個性:Flux 是成功的嗎?

當然是,如今不計其數的網站也應用在使用 React 和 Flux;並且就像我上面提到的,即使是六年以後,在它之後的框架絕大部分是它的追隨者而非顛覆者,都能找到 Flux 的影子。但在它誕生之初,無論是在 Reddit, Youtube,還是 InfoQ 上甚至至今為止都有批評的聲音,

但在你的那些使用了 Flux 的項目中,有多少項目在可維護性上是成功的?如何定義可維護性呢,我們用 Uncle Bob 的三個標準來回答這一個問題:

  • It is hard to change because every change affects too many other parts of the system.(Rigidity)

  • When you make a change, unexpected parts of the system break. (Fragility)

  • It is hard to reuse in another application because it cannot be disentangled from the current application. (Immobility)

    —— The Principles of OOD

我相信答案在各位心中已經呼之欲出了。站在工程師的角度上看項目代碼的可維護性並不取決於你使用的框架多麼的先進,而是取決於使用框架的和內部的工程師文化

扯遠了,說回 Flux。

在這裡我不會再聊 Flux 的那些基本入門概念。我們重點說一說 Flux 解決的問題,幫助你更好的理解 Flux

不知道有多少人看過 Flux 走向公眾視野的第一個視頻 Hacker Way: Rethinking Web App Development at Facebook ,這個視頻上透露了很多有助於關於我們理解 Flux 的很多信息。這一節的內容不少複述自該視頻

首先要強調的是,Flux 並非是為了顛覆和創新而生,而是為了解決我們所說的非功能性需求。

在 Facebook 公司內部工程師需要保證交付軟件的質量,但是高質量意味著需要花費更多的時間;但另一方面公司也希望崇尚 move fast 的核心價值,也就是要用更少的時間交付更多的價值,於是這兩者間似乎產生了矛盾,如何用更少的時間交付更高質量的軟件。

用視頻中的原話說,按照順序他們想達成的目標是

  • Produce higher (quality) code
  • Higher quality software
  • Better code by default
  • We want to do it in less time
前端架構101(四):MVC的不足與Flux的崛起

其中有意思的是第三點:「Better code by default」。在我看來這就是我在第一篇中強調的 「Falling Into The Pit of Success」有異曲同工之妙(你也可以說我現在眼裡只有錘子看什麼都是釘子)——你要讓你的開發人員一開始(容易)就寫出對的代碼。

而在他們的項目中最大的阻礙竟然是 MVC 架構

整個宣講 Flux 過程中最令人詬病的就是這一張圖,在我上面提到的批評聲音中,最共同的聲音就是它們以一種錯誤的方式實施了 MVC,所以才導致了他們的應用無法拓展。時候演講者 Jing Chen 也承認演示中的圖片確實投機取巧了。它們真正想表達的是這種雙向的數據流架構會產生一定的負面效應

前端架構101(四):MVC的不足與Flux的崛起

首先就像我在前幾篇中提到的那樣,從客戶端到後端到前端並沒有“標準的 MVC” 一說。即使你只在前端領域內尋找統一的 MVC 概念,你也會發現從 Backbone.js, AngularJS 到 Ember.js 的實現各不相同。

正因為大家對 MVC 的理解各不相同,甚至在同一個框架內也沒有推薦的最佳實踐,於是你會看到在一個框架內解決一個問題的不同實現。其中有一些方案是存在隱患的,但是在小規模的應用內很難暴露出來。但隨著團隊的擴充和複用代碼的越來越多,代碼會變得越來越脆弱,因為不同人看到同一份代碼的理解是不同的。上圖中的情況是非常有可能發生的,但並非是按照上圖一模一樣的方式,但後果就是跨職責和意料之外的級聯更新。

如果你現在站在開發 React 應用的體驗上看 Backbone.js 和 AngularJS 的開發體驗,你會感覺框架帶來的約束是鬆散的。以 AngularJS 為例,它賦予了你 controller / view 機制,但至於是在多個 view 之間共享 controller,又或者相對於一個 view 嵌套多層 controller,它完全不做任何的限制。這就極易產生上述後果。在下圖中 View C 可以訪問和修改多個祖先 controller 中的變量(左側黃色箭頭)同時變量又有可能會被 View B 和 View C 使用(右側藍色箭頭)。

前端架構101(四):MVC的不足與Flux的崛起

所以你現在理解了為什麼 Flux 會嘗試用單向數據流解決這個問題了。我們抽取 store 來保證唯一數據源(single source of truth),所有的業務邏輯也都封裝在 store 中,避免了用例和服務層(對應後端 service layer)方法散落在各個 controller 中。注意 store 層工作是不會引起任何的副作用的,在 store 完成上一個 action 的工作之前,不會有其他的 action 再次經過 dispatch 達到 store。同時使用 command 模式來避免事件機制造成的的不可預測性。剩下的具體概念你應該非常熟悉了

現在回過頭再看 Flux,它其實是一個非常強約束的框架。假設你需要完成一項工作,比如接住後端傳遞的用戶信息裡的新增字段,你會非常明確的知道你需要修改 store, 該 view,而不需要修改 action。到了在 store 中新增字段的這一個環節,無論是你是使用 Redux 還是 Mobx 相信你都能迅速的找到對應的 model / reducer 在哪。我想這就是 期望中的 「Better code by default」 吧

簡單聊框架的約束

就像我一開始提到的,目前的框架傾向於為你提供應用級別的整套解決方案。並且極其詳細的為你劃分模塊。最初我們只有 model, controller 和 view;但現在我們有 component, store / reducer, action, dispatch, selector / query, reducer, service, effect, 甚至在有的框架中還有更細化的 entity store, entity query。所以當你現在需要開發一個功能時,你能夠很輕易的把你的需求拆解為對應的模塊,分別把它們開發、測試完畢之後接入應用即可。

有人認為如此強的職責劃分和框架約束扼殺了編程的創造力和樂趣。但我認為工業化的代碼產出穩定和高效才是最重要的。

現代的前端技術棧已經變得非常複雜了,「精通」已經成為了一件奢侈的事,更別說讓整個團隊達到相同的「精通」水平。如果你開發的是 Angular 應用,Angular 本身,或是 Rxjs 又或是 TypeScript 哪一個單拎出來都不好對付,指望著人們自我學習或者培訓的方式統一大家的水平更是天方夜譚。目前看來「約束」看起來是最簡單也是最靠譜的方式。這種約束體現的不僅是在模塊的明碼標價設計上,還可以體現在使用 lint,TypeScript 等語法的約束上

相關文章

vue2.x源碼解析系列二:Vue組件初始化過程概要

關於Vue和React區別的一些筆記

三分鐘從零單排js靜態檢查

前端架構101(五):從Flux進化到ModelViewPresenter