導(dǎo)論
什么是 Redux
Redux 是 Flux 的一個變種, 是一種非常流行的單向數(shù)據(jù)流管理框架. 在 Flux 的基礎(chǔ)上, Redux 將數(shù)據(jù)流進(jìn)行統(tǒng)一的管理, 讓數(shù)據(jù)流的變化, 變得可預(yù)測, 這也是 Redux 名字的由來.
我眼中的 Redux
Flux 早期的官方實現(xiàn)中, 并沒有 Reducer 的概念, Store 是一種服務(wù)的聚合體, 包括一些數(shù)據(jù)和操作, 而 Redux 中的 Store 已經(jīng)是另一種概念了, 如果說 Flux 的早期實現(xiàn)是一個 OOP 的版本, 那 Redux 其實算是 Flux 的 FP 實現(xiàn), 所以當(dāng)我們將 OOP 的各種設(shè)計工具應(yīng)用到基于 Redux 構(gòu)建的應(yīng)用中去時, 你會發(fā)現(xiàn)如此的格格不入, 一切都是離散的, 一個對象被變成了三部分, Action, Reducer, Store 中的一部分, 由于 OOP 在軟件開發(fā)領(lǐng)域的巨大影響, 為了能夠融入 OOP 社區(qū)做出了很多努力和嘗試, 其中典型的代表是基于 Duck-Module 的流派, 如阿里的 dva 等, 另外一些則屬于原生派, 通過各種 Util 來減少 Redux 的樣板代碼. 不過本文不討論這些實現(xiàn), 本文將討論的是如何在 Redux 上實現(xiàn)這兩種風(fēng)格的前端應(yīng)用架構(gòu)
基于 Redux 的 OOP 前端應(yīng)用架構(gòu)
OOP 的核心概念是類和對象, 我們開發(fā)類來封裝數(shù)據(jù)和操作, 提供對象來輸出它們, 通過組合和繼承來描述復(fù)雜的對象體系. 用各種工具來確保操作的邊界, 讓對象高內(nèi)聚, 同時隔離彼此之間的風(fēng)險.
所以如果沒有對象, 一切就都是徒勞的.
而 Redux 是基于 FP 的, 就像兩種不同的世界觀, 無論怎么擰巴, Reducer 也不可能成為 OOP 里的一員, Action 只是消息的傳遞者, 在 OOP 里我們通過給對象發(fā)送消息來實現(xiàn)方法的調(diào)用, 從這個角度看, Bind 了 Dispatch 的 ActionCreator 看起來更像是一個方法, 但是數(shù)據(jù)又都在 Store 里, 但仔細(xì)思考下, 結(jié)合以上這幾點, 我們大致上應(yīng)該能理出一條通向 OOP 的道路
- 隱藏 Reducer
- 隱藏 Dispatch, ActionCreator 默認(rèn)綁定 Dispatch
- 將對象掛載到 Store 上, 隱藏 getState() API
有了思路, 那就開始搞吧
搞吧....其實本文只討論編程思想, 所以如何實現(xiàn), 就得另起一篇了:-D
基于 Redux 的 FP 前端應(yīng)用架構(gòu)
討論完了 OOP, 然我們來看看 FP, FP 的核心不用說自然是函數(shù)了, 一切皆函數(shù), 但要開發(fā)一個應(yīng)用, 僅僅光靠函數(shù)這一個概念, 我們的工作量未免過于巨大, 因此需要引入一些有效的工具幫助我們, 比如 "管道" 和 "高階函數(shù)", 另外為了保持?jǐn)?shù)據(jù)不可變, 我們還需要引入不可變的數(shù)據(jù)類型系統(tǒng) "Immutable", 有了這些我們可以嘗試開始著手去搭建一個前端應(yīng)用架構(gòu)了.
如果說 OOP 是用一堆對象描述世界, 對象彼此通過消息來傳遞數(shù)據(jù), 那在 FP 的角度來看, 世界就像一條條管道, 數(shù)據(jù)在管道中流動, 管道和管道之間可以并行, 也可以串聯(lián), 甚至可以彼此包含. 在對象的世界里, 對象負(fù)責(zé)存儲和處理數(shù)據(jù), 數(shù)據(jù)是動態(tài)的, 可變的, 而在管道的世界里, 數(shù)據(jù)是不可變的, 因為管道不負(fù)責(zé)存儲數(shù)據(jù), 如果數(shù)據(jù)沒有任何消費方, 那數(shù)據(jù)本身就沒有意義了. 所以存儲數(shù)據(jù)是不必要的. 因為數(shù)據(jù)處理本身并不綁定數(shù)據(jù), 這也使得數(shù)據(jù)處理的過程變得非常靈活. 總結(jié)下, FP 的前端應(yīng)用架構(gòu)基于以下四點
- 用函數(shù)定義操作, 并解決數(shù)據(jù)并行的問題
- 用高階函數(shù)來組合函數(shù), 構(gòu)建復(fù)雜的操作
- 用管道解決數(shù)據(jù)的串行操作
- 引入"Immutable" 保證數(shù)據(jù)不可變
基于這四點, 我們可以嘗試對前端應(yīng)用架構(gòu)使用 FP 的方式來進(jìn)行抽象和描述, 比如對于一個包含界面的 Web應(yīng)用, 從用戶輸入數(shù)據(jù)到傳遞給服務(wù)端的過程, 可以想象成一條管道
用戶輸入 -> UI響應(yīng)并處理 -> 數(shù)據(jù)層響應(yīng)并處理 -> 發(fā)送數(shù)據(jù)的管道響應(yīng)并處理 -> 數(shù)據(jù)源響應(yīng)并處理 -> 持久化
//反過來
用戶請求 -> UI響應(yīng)并處理 -> 數(shù)據(jù)層響應(yīng)并處理 -> 發(fā)送數(shù)據(jù)的管道請求并處理 -> 數(shù)據(jù)源響應(yīng)并發(fā)送
在過去要保證這樣的串行操作, 可能存在諸多問題, 比如異步的處理, 所幸 ES7的 async/await 給我們帶來了便利, 異步和同步編程模型的統(tǒng)一使得降低了前端架構(gòu)的復(fù)雜性. 整個前端應(yīng)用架構(gòu)看上去就像兩條流水線, 組合在一起就是一個環(huán), 我叫它"環(huán)形架構(gòu)"或者"流水線架構(gòu)"
以其中的數(shù)據(jù)管道為例, 我們可以想象請求數(shù)據(jù)的引擎就是一個并行節(jié)點, 因為對不同的數(shù)據(jù)源, 請求引擎可能不同, 但是我們會統(tǒng)一成一個概念, 比如 fetch, 在 node 和微信小程序下就是完全不同的, 但是通過概念統(tǒng)一, 我們可以利用高階函數(shù)組合出一個能夠并行處理多種數(shù)據(jù)源的引擎, 利用管道對傳遞的參數(shù)和數(shù)據(jù)源響應(yīng)的數(shù)據(jù)分別處理.
事實上, 前端架構(gòu)中的很多部分都無法用對象描述, 包括層次, 垂直的和縱向的, 流程圖, 狀態(tài)圖, 時序圖等等. 但這些東西卻可以用數(shù)據(jù)管道來統(tǒng)一描述, 因為前端架構(gòu)圖都是平面圖, 當(dāng)我們描述前端架構(gòu)的時候, 都是在同一平面上描述數(shù)據(jù)是如何流動和處理的, 無論我們?nèi)绾蝿澐? 數(shù)據(jù)在某一平面上一定是某一種串行或者并行操作.
當(dāng)你習(xí)慣這種思維模式, 我想你會上癮, 因為一切都是一個環(huán)上的某個操作, 無論多復(fù)雜的系統(tǒng), 都可以被分解成一個簡單的操作, 粒度的控制完全取決于你.
然后我們把 Redux 放到這個應(yīng)用的環(huán)中, 你會發(fā)現(xiàn), Redux 代表的并不是整個環(huán), 而是其中的一部分, 更像是一個DB, Store 提供 Dispatch 和 來觸發(fā)數(shù)據(jù)的寫入, Action.Type 如果用來表示數(shù)據(jù)類型顯然比用來描述操作更合理, 這樣 Action 本身就是可枚舉的了, 相對應(yīng)的 Reducer 也變得可枚舉了, 然后整個前端應(yīng)用的架構(gòu)應(yīng)該是
UI 數(shù)據(jù)管道 -> Redux 管道 -> 數(shù)據(jù)源處理管道 -> 數(shù)據(jù)源
我們將 UI 上的 local 數(shù)據(jù)放在 UI 數(shù)據(jù)管道處理, 同時 Redux 管道負(fù)責(zé)接管外部數(shù)據(jù)源的讀寫, 而數(shù)據(jù)源處理管道則負(fù)責(zé)通信中前后的數(shù)據(jù)處理, 如果應(yīng)用很龐大, 你可以加入更多的平面, 去切分整個系統(tǒng), 例如加入數(shù)據(jù)模型的重組, 比如使用 Reselect 庫重組數(shù)據(jù)模型
UI 數(shù)據(jù)管道 -> 數(shù)據(jù)模型重組管道 -> Redux 管道 -> 數(shù)據(jù)源處理管道 -> 數(shù)據(jù)源
只要你愿意, 你可以合理的切分更多的平面出來, 當(dāng)然過多的管道會帶來通信和數(shù)據(jù)校驗的成本, 這些都是需要在架構(gòu)設(shè)計中被考慮到的部分.
但當(dāng)你在你的應(yīng)用中使用這種架構(gòu)基于 Redux 去開發(fā), 你的應(yīng)用應(yīng)該會非常容易測試和維護(hù), 但是在開發(fā)模式上, 采用 TDD 應(yīng)該會更合適, 因為確保每個管道的可用性, 是將他們拼起來的唯一前提.
某野生前端架構(gòu)師
寫于2017年夏末