基於 MVC 理解 React+Redux

張_逸發表於2019-03-04

我認為MVC模式雖然已經誕生了許多年,也有無數前端框架遵循了MVC模式,但我們在前端開發時,很多時候還是忽略了這個模式蘊含的思想。該思想的核心就是職責分離,這種分離又隱含了“資訊專家模式”的意義,直白地說,就是“專業的事情應該交給專業的人去做”。

MVC(Model-View-Controller)的三個角色其實是各司其職:

  • model持有UI要展現的資料
  • View即UI的展現
  • Controller用於控制

以React來說,它就應該只專注於View的呈現,並將這些展現元素封裝為Component。這些Component要展現的props可以視為Model所持有的資料。

那麼,什麼情況下會導致View產生變化呢?從表象上看,似乎引起變化的原因是由於客戶端的某種請求或互動操作產生的事件。實則從業務上說,其實就是要改變Model的值,而UI的互動操作不過是對這種變化的介面展現罷了。換言之,View的變化其實應該通過Model的變化來傳遞

當我們需要改變View時,一種做法是直接在View上做文章,通過編寫針對UI元素的控制邏輯去改變View。另一種做法就是遵循MVC模式,應該通過Controller去改變Model的結構,然後通知View去改變自己(或者理解為View偵聽到Model的變化,從而改變自己)。

React結合Redux框架做的正是這樣的事情。在設計React Component時,我們需要通過UI的Layout來規劃我們的Component,包括Component的分解與組合。呈現Component的過程就可以抽象為一個函式,這個函式接收一個輸入物件model,返回一個包裹了HTML元素與Model的DOM`結構。如以下虛擬碼:

const render = (model) => DOM複製程式碼

如果業務邏輯要求操作View的DOM,其實就是對DOM包裹的Model進行操作,例如新增或修改某個<li>,其本質是要新增或修改<li>元素中的值,這個值來自於Model。在Redux中,其實就是發起一個action

執行action的目的雖然是修改Model,不過在Redux中,我們儘量希望遵循FP的思想設計出所謂的“純函式”,於是Redux就引入了reducer函式,這個函式要做的事情其實就是對Model進行transform(可以考慮引入immutable.js來儲存和操作Modle)。一旦Model物件發生了變化(並不是真正發生了變化,而是產生了一個新的Model),Redux就會通知React Component根據新獲得的Model去重新Render。

顯然,React扮演的是View的角色,Redux則是Controller,至於Model就是Redux Store中儲存的State。我們要從MVC模式的角度去思考React+Redux開發,把程式碼需要做的每件事情想清楚,明確是誰的職責,如此才不至於在實現時走歪路,不討好地去編寫大量View的控制邏輯,尤其是那些牽涉到parent-child元件的遞迴關係時,可能會讓前端程式碼燉成一鍋粥。

舉個例項。

我們要在前端編寫一個過濾器,UI展現與控制邏輯類似Logiform,如下圖所示:

基於 MVC 理解 React+Redux

這個過濾器可以理解為以Condition為根的一個遞迴巢狀樹形結構,枝為Group,而葉為ExpressionGroup還可以巢狀Group或者Expression。可以新增、刪除GroupExpression,也可以調整它們在樹中所處的位置。

針對這樣的需求,如果我們企圖在React Component中直接去操控和管理這些邏輯,就需要考慮Component的父子關係,還需要考慮新增或刪除Dom節點對整棵樹的影響。

如果我們站在前述MVC模式的角度來考慮過濾器樹的呈現與介面控制,其實不過就是針對Condition物件模型的操作罷了。這個時候,我們可以不用去操心DOM節點之間的關係,而是直接用React Component去render模型物件。物件的粗略結構如下所示:

{ 
  "id": 1, 
  "operator": "and", 
  "conditions": [
    {
      "id": 2,
      "type": "expression",
      "operator": "=",
      "fieldId": "11000",
      "value": 3
    },
    {
      "id": 3,
      "type": "group",
      "operator": "or",
      "conditions": [
        {
          "id": 4,
          "type": "expression",
          "operator": "<=",
          "fieldId": "11001",
          "value": 20
        }
    ]
  }]
}複製程式碼

由於render是一種只讀的操作,要在React Component中去render這樣的結構是非常容易的。如上,當我們要刪除id為2的Expression時,其實就是去編寫一個reducer,將其轉換為如下的物件:

{ 
  "id": 1, 
  "operator": "and", 
  "conditions": [
    {
      "id": 3,
      "type": "group",
      "operator": "or",
      "conditions": [
        {
          "id": 4,
          "type": "expression",
          "operator": "<=",
          "fieldId": "11001",
          "value": 20
        }
    ]
  }]
}複製程式碼

render對UI的呈現與控制邏輯完全相同,並不需要再去控制複雜的DOM。

概況下來,React+Redux的主體流程為:

  • 通過action獲得model,並將其作為state儲存到Store中;
  • 傳遞給React Component,按照某種設計呈現model資料;
  • 呼叫action發起update請求,從而呼叫reducer生成新的state儲存到Store中;
  • redux通知React Component重新Render。

這是MVC三種角色各司其職相互協作的結果。

相關文章