- 原文地址:UNIDIRECTIONAL USER INTERFACE ARCHITECTURES
- 原文作者:André Staltz
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:EmilyQiRabbit
- 校對者:Hopsken,dandyxu
單向使用者介面架構
本文對所謂的“單向資料流”架構進行了非詳盡的概述。這並不意味著本文應被視為一個初學者教程,它更應該是一個架構之間的差異和特性的概述。最後,我將會介紹一個和其他框架顯著不同的新框架。本文僅假設客戶端是 Web UI 框架。
術語
如果沒有術語的共識,討論這些框架可能會造成困惑,所以我們作出如下的假設:
使用者事件(User events) 是來自使用者直接操作的輸入裝置的事件。比如:滑鼠點選,滑鼠滾動,鍵盤按鍵,螢幕觸控等等。
當不同的框架使用 “View” 這個術語時,含義可能大不相同。作為替代,我們使用 “rendering” 來代表共識中的 “View”。
**使用者介面渲染(User interface rendering)**指代螢幕上的圖形輸出,一般情況下用 HTML 或者其他類似的高階宣告程式碼比如 JSX 來描述。
一個**使用者介面(UI)程式(User interface (UI) program)**是任何一個將使用者事件作為輸入輸出檢視的程式,這是一個持續的過程而不是一次性的轉換。
假定 DOM 以及其他層比如一些框架和庫存在於使用者和架構之間。
模組間箭頭的所屬很重要。A--> B
和 A -->B
是不一樣的。前者是被動程式設計,而後者是反應式程式設計。這裡可以閱讀更多。
如果子元件和整體的結構一致,這個單向架構就被稱為分形(fractal)。
在分形架構中,整體可以像元件一樣簡單地打包然後用於更大的應用。
在非分形架構中,那些不重複的部分被稱為協調器(orchestrators),它們不屬於具有分級結構的部分。
FLUX
第一個必須提到的是 Flux。它雖然不是絕對的先驅,但是至少在流行度上,對於很多人它都是第一個單向架構。
組成部分:
- Stores:管理事務資訊和狀態
- View:一個 React 元件的分級結構
- Actions:由 View 當中觸發的使用者事件而產生的事件
- Dispatcher:搭載所有 actions 的事件
特點:
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 得出新的狀態
特點:
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 的動態屬性
特點:
多範例。 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 轉變為新的狀態
特點:
到處都是分級結構。 之前的幾個框架只在 “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 是可選的。
特點:
極大的依賴於 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”:
如圖所示,一個 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 重組後的樣子:
要想將 Dialogue 方法作為一個更大程式的 UI 程式子元件重複使用,這就涉及到 Dialogue 之間的巢狀問題:
Observables 在 Dialogues 不同層之間的連線是一個資料流圖。它並不必須是一個非週期圖。在例如子元件動態列表這樣的例項中,資料流圖就必須是週期的。這樣的例子超出了本文的討論範圍。
巢狀的 Dialogues 實際上是一個元架構:它對元件的內部結構沒有約束,這就允許我們將前文所述的所有架構嵌入一個巢狀的 Dialogue 元件中。唯一的約束涉及 Dialogue 的一端的介面:輸入和輸出都必須是一個或一組 Observable。如果一個結構如同 Flux 或者 Model-View-Update 的 UI 程式能夠讓它的輸入和輸出都以 Observables 呈現,那麼這個 UI 程式就能夠作為一個 Dialogues 函式嵌入一個巢狀的 Dialogues。
因此,這個架構是分形的(僅涉及 Dialogue 介面時)、一般性的。
可以檢視 TodoMVC implementation 和 this 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 僅僅是我現在的最強烈推薦。
如果你喜歡這篇文章,分享給你的 followers:(tweeting)。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。