[譯] 單向使用者介面架構

玉兒Qi發表於2018-04-08

單向使用者介面架構

本文對所謂的“單向資料流”架構進行了非詳盡的概述。這並不意味著本文應被視為一個初學者教程,它更應該是一個架構之間的差異和特性的概述。最後,我將會介紹一個和其他框架顯著不同的新框架。本文僅假設客戶端是 Web UI 框架。

術語

如果沒有術語的共識,討論這些框架可能會造成困惑,所以我們作出如下的假設:

使用者事件(User events) 是來自使用者直接操作的輸入裝置的事件。比如:滑鼠點選,滑鼠滾動,鍵盤按鍵,螢幕觸控等等。

當不同的框架使用 “View” 這個術語時,含義可能大不相同。作為替代,我們使用 “rendering” 來代表共識中的 “View”。

**使用者介面渲染(User interface rendering)**指代螢幕上的圖形輸出,一般情況下用 HTML 或者其他類似的高階宣告程式碼比如 JSX 來描述。

一個**使用者介面(UI)程式(User interface (UI) program)**是任何一個將使用者事件作為輸入輸出檢視的程式,這是一個持續的過程而不是一次性的轉換。

假定 DOM 以及其他層比如一些框架和庫存在於使用者和架構之間。

模組間箭頭的所屬很重要。A--> BA -->B 是不一樣的。前者是被動程式設計,而後者是反應式程式設計。這裡可以閱讀更多。

如果子元件和整體的結構一致,這個單向架構就被稱為分形(fractal)

在分形架構中,整體可以像元件一樣簡單地打包然後用於更大的應用。

在非分形架構中,那些不重複的部分被稱為協調器(orchestrators),它們不屬於具有分級結構的部分。

FLUX

第一個必須提到的是 Flux。它雖然不是絕對的先驅,但是至少在流行度上,對於很多人它都是第一個單向架構。

組成部分:

  • Stores:管理事務資訊和狀態
  • View:一個 React 元件的分級結構
  • Actions:由 View 當中觸發的使用者事件而產生的事件
  • Dispatcher:搭載所有 actions 的事件

Flux diagram

特點:

Dispatcher。 因為它是事件的載體,它是唯一的。很多 Flux 的變體去掉了對 dispatcher 的需求,其他的一些單向框架也沒有 dispatcher 等同物。

只有 View 有可組合元件。 分級結構僅存在於 React 元件中,Stores 和 Actions 都沒有。一個 React 元件就是一個 UI 程式,並且其內部通常不會編寫成一個 Flux 架構的形式。所以 Flux 不是分形的,Dispatcher 和 Stores 作為它的協調器。

使用者事件處理器在 rendering 中宣告。 換句話說,React 元件的 render() 函式處理和使用者互動的兩個方向:渲染和使用者事件處理(例如 onClick={this.clickHandler}

REDUX

Redux 是一個 Flux 的變體,單例 Dispatcher 被改編成了一個獨一的 Store。Store 不是從零開始實現的,相反,建立它的方式是給 store 工廠一個 reducer 函式。

組成部分:

  • Singleton Store:管理狀態,並擁有一個 dispatch(action) 函式
  • Provider:Store 的訂閱者,和像 React 或者 Angular 這樣的 “View” 框架互動
  • Actions:由使用者事件建立的事件,並且是根據 Provider 而建立
  • Reducers:純函式,根據前一狀態和一個 action 得出新的狀態

Redux diagram

特點:

store 工廠。 使用工廠函式 createStore() 可以建立 Store,由 reducer 函式作為組成引數。還有一個元工廠函式 applyMiddleware(),接受中介軟體函式作為引數。中介軟體是用附加的鏈式功能重寫 store 的 dispatch() 函式的機制。

Providers。 對於用來作為 UI 程式的 “View” 框架,Redux 並不武斷控制。它可以和 React 或者 Angular 或者其他框架配合使用。在這個框架中,“View” 是 UI 程式。和 Flux 一樣,Redux 被設計為非分形的,並且以 Store 作為協調器。

使用者事件處理函式的宣告可能在也可能不在 rendering。 取決於當下的 Provider。

BEST

Famous Framework 引入了 Behavior-Event-State-Tree (BEST),它是一個 MVC 的變體,BEST 中 Controller 分成了兩個單向元素:Behavior 和 Event。

組成部分:

  • State: 用類 JSON 結構的宣告來初始化 state
  • Tree: 一個元件的宣告性分級結構
  • Event: 在 Tree 上的事件監聽,它能改變 state
  • Behavior: 依賴 state 的 tree 的動態屬性

BEST diagram

特點:

多範例。 State 和 Tree 是完全宣告式的。Event 是急迫性的,Behavior 是功能性的。一些部分是響應式的,而其他部分則是被動式的。(例如,Behavior 會對 State 作出反應,Tree 則對 Behavior 比較消極)

Behavior。 Behavior 將 UI 檢視(Tree)和它的動態屬性分離了,這在本文中的其他幾個框架中都不會出現。據稱,這出於不同的考慮:Tree 就好比 HTML,Behavior 就好比 CSS。

使用者事件處理的宣告從檢視分離。 BEST 是極少的不將使用者事件處理和檢視關聯的單向框架之一。使用者事件處理屬於 Event,而不是 Tree。

在這個框架中,“View” 是一個樹結構,一個 “Component” 是一個 Behavior-Event-Tree-State 元組。元件是 UI 程式。BEST 是分形框架。

MODEL-VIEW-UPDATE

也被稱為 “The Elm Architecture”,Model-View-Update 和 Redux 很相似,主要因為後者是受這個框架啟發的。這是一個純函式的框架,因為它的主語言是 Elm,一個 Web 的函數語言程式設計語言。

組成部分:

  • Model:一個定義狀態資料結構的型別
  • View:將狀態轉化為檢視的純函式
  • Actions:定義通過郵件傳送的使用者事件的型別
  • Update:一個純函式,將前一狀態和 action 轉變為新的狀態

Model-View-Update diagram

特點:

到處都是分級結構。 之前的幾個框架只在 “View” 中有分級結構,但是在 MVU 架構中這樣的結構在 Model 和 Update 中也能找到。甚至是 Actions 可能也巢狀了 Actions。

元件分塊匯出。 因為哪裡都是分級結構,在 Elm 架構中的 “component” 是一個元組,包括了:模組型別,一個初始模組例項,一個 View 函式,一個 Action 型別,一個 Update 函式。縱覽整個架構,不可能有元件從這個結構中偏離。每個元件都是 UI 程式,並且這個架構是分形的。

MODEL-VIEW-INTENT

Model-View-Intent 是基於框架 Cycle.js 的主要架構模式,它同時也是基於觀察者 RxJS 的完全反應單向架構。可觀察(Observable) 事件流是一個所有地方都用到的原函式,Observables 上的函式是架構的一部分。

組成部分:

  • Intent:來自 Observable 使用者事件的函式,用來觀察 “actions”
  • Model:來自 Observable 的 actions 的函式,觀察 state
  • View:來自 Observable 的 state 的函式,觀察 rendering 檢視
  • Custom element:rendering 檢視的子部件,其自身也是一個 UI 程式。可能會作為 MVI 或者一個 Web 元件被應用。是否應用於 View 是可選的。

Model-View-Intent diagram

特點:

極大的依賴於 Observables。 該框架每一部分的輸出都被描述為 Observable 事件流。因此,如果不用 Observables,就很難或者說不可能描述任何 “data flow” 或 “change”。

Intent。 和 BEST 中的 Event 大致相似,使用者事件處理在 Intent 中宣告,從檢視中分離出來。和 BEST 不同,Intent 建立了 actions 的 Observable 流,這裡的 actions 就和 Flux,Redux,和 Elm 中的類似。但是,和 Flux 等中的不同的是, MVI 中的 actions 不直接被髮送到 Dispatcher 或 Store。它們就是簡單的可以直接被模組監聽。

完全反應。 使用者檢視反應到檢視輸入,檢視輸出反應到模組輸出,模組輸出反應到 Intent 輸出,Intent 輸出反應到使用者事件。

MVI 元組是一個 UI 程式。當且僅當所有使用者定義元素與 MVI 一起應用時,這個框架是分形的。

NESTED DIALOGUES

這篇博文將 Nested Dialogues 作為一個新的單向架構來介紹,適用於 Cycle.js 和其他完全依賴於 Observables 的方法。這是 Model-View-Intent 架構的一次進化。

從 Model-View-Intent 序列可以函式化組合為一個函式這個特性說起,一個 “Dialogue”:

A Dialogue function equivalent to Model-View-Intent

如圖所示,一個 Dialogue 是一個將使用者事件的 Observable 作為輸入(Intent 的輸入),然後輸出一個檢視的 Observable(View 的輸出)的方法。因此,Dialogue 就是一個 UI 程式。

我們推廣了 Dialogue 的定義來容許使用者之外的其他目標,每一個目標都有一個 Observable 輸入和一個 Observable 輸出。例如,如果 Dialogue 通過 HTTP 連線了使用者和服務端,這個 Dialogue 就應該接受兩個 Observables 作為輸入:使用者事件的 Observables 和 HTTP 響應的 Observables。然後,它將會輸出兩個 Observables:檢視的 Observables 和 HTTP 請求的 Observables。這個是 Cycle.js 裡面 Drivers 的概念。

這就是 Model-View-Intent 作為 Dialogue 重組後的樣子:

A Dialogue function as a UI program

要想將 Dialogue 方法作為一個更大程式的 UI 程式子元件重複使用,這就涉及到 Dialogue 之間的巢狀問題:

Nested Dialogues

Observables 在 Dialogues 不同層之間的連線是一個資料流圖。它並不必須是一個非週期圖。在例如子元件動態列表這樣的例項中,資料流圖就必須是週期的。這樣的例子超出了本文的討論範圍。

巢狀的 Dialogues 實際上是一個元架構:它對元件的內部結構沒有約束,這就允許我們將前文所述的所有架構嵌入一個巢狀的 Dialogue 元件中。唯一的約束涉及 Dialogue 的一端的介面:輸入和輸出都必須是一個或一組 Observable。如果一個結構如同 Flux 或者 Model-View-Update 的 UI 程式能夠讓它的輸入和輸出都以 Observables 呈現,那麼這個 UI 程式就能夠作為一個 Dialogues 函式嵌入一個巢狀的 Dialogues。

因此,這個架構是分形的(僅涉及 Dialogue 介面時)、一般性的。

可以檢視 TodoMVC implementationthis small app 作為使用了 Cycle.js 的巢狀 Dialogues 的例子。

重點總結

儘管巢狀 Dialogues 的一般性和優雅性在理論上可以用來作為子元件嵌入到其他架構中,但我對這個框架最主要的興趣在於構建 Cycle.js 應用。我一直在尋找一個自然靈活的 UI 架構,並且同時能夠提供 結構

我認為巢狀的 Dialogues 是自然的,因為它直接表現了其他典型 UI 程式完成的:一個將使用者事件作為輸入(輸入 Observable)持續執行的程式(Observable 就是持續的程式),並且產生檢視作為輸出(輸出 Observable)。

它也是靈活的,因為正如我們所見,Dialogue 的內部結構可以自由的應用於任何模式。這和有著死板結構作為條框的 Model-View-Update 截然相反。分形架構比非分形的更加易重用,我很高興巢狀的 Dialogues 也有這個屬性。

但是,一些常規的結構也可以對引導開發有所幫助。雖然我認為 Dialogue 的內部結構應當是 Flux,但我想 Model-View-Intent 很自然的適配了 Observable 的輸入輸出介面。所以當我想自由一些,不把 Dialogue 作為 MVI 時,我承認大部分時間我都會把它構造成 MVI。

我不想自大的說這是最好的使用者介面架構,因為我也是剛剛發現了它並且依舊需要實際應用來發現它的優缺點。巢狀 Dialogues 僅僅是我現在的最強烈推薦。


Comments in Hacker News.

如果你喜歡這篇文章,分享給你的 followers:(tweeting)


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章