對於學習 Redux 的一些建議
React 和 Redux 經常結合在一起使用,Redux 是 flux 架構模式的一種優秀實現,並且在 React 社群被廣泛使用,但也不是完全和 React 耦合在一起的。
全域性 state
並不是所有的全域性state都需要被儲存起來,一些元件可以使用 setState 來管理元件的內部狀態,這也是為什麼在學習 Redux 前要掌握 React 中的 setState ,否則你將習慣式的把所有的global state都儲存在store裡面。所以思考一下,在大型開發團隊裡面開發的複雜應用,你更不能將應用的所有 state 都切換成全域性狀態。
專案目錄如何組織
這篇文章organizing-redux-application 給出了三種建議方式來組織專案結構。
第一種方式是按功能劃分
React + Redux 的一些教程經常給我們展示按功能劃分的目錄,這也是一種很好的 React + Redux 學習方式,不過,將應用的所有 reducers 和 actions 都放在專門的資料夾維護的方案,並不是所有人都能贊同。
1 2 3 4 |
src/ --actions/ --reducers/ --components/ |
經常聽到的有建設性的想法是,目錄劃分應該以元件為核心,每個目錄應該有元件本身以及它所對應的 reducers、actions,那麼一個示例的目錄結構應該是這樣的:
1 2 3 4 |
message/ --components --reducer.js --actions.js |
一個包含 container component 、presenter component以及測試相關的詳細的元件目錄會是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
message/ --components/ ----messageItem/ ------presenter.js ------spec.js ----messageList/ ------container.js ------presenter.js ------spec.js --reducer/ ----index.js ----spec.js --actions/ ----index.js ----spec.js |
當然了,也並不是大家都會喜歡這種方式。(其實,我個人是很贊同這樣的就近維護元件的原則的,因為將各個功能性的reducer和action都丟到對應的目錄,這以後維護起來會更加困難,檔案也不好找,這可不像是MVC那樣的分層結構。)尤其是將reducer隱藏在各個功能目錄中,這也不利於全域性性的來理解使用 redux 的架構意圖。所以建議是適當的在最初就抽取一些 reducers 來共享他們所包含的功能。
但在現實場景中,尤其是多個團隊在同一個應用專案中協作的時候,在開發進度的壓力之下,並沒有那麼多機會來正確的抽象出一些 reducers。反而通常是一口氣的封裝所有的功能模組,只為了感覺把活給幹完了,讓需求按時上線。
第二種方式是對功能模組劃分清晰的界限
給每個模組都設定一個 index.js
檔案作為入口,這個檔案只是用於匯出一些API給其他的模組使用。在基於 React + Redux 的應用中,index.js
檔案可以用於匯出一個 container components ,或是一個presenter components、action creators、能用於其他地方的 reducer(但不是最終的reducer)。那麼,基於這樣的思考,我們的目錄就可以變成這樣了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
message/ --index.js --components/ ----messageItem/ ------index.js ------presenter.js ------spec.js ----messageList/ ------index.js ------container.js ------presenter.js ------spec.js --reducer/ ----index.js ----spec.js --actions/ ----index.js ----spec.js |
那麼,在當前功能模組下的 index.js 檔案應該包含這些程式碼:
1 2 3 4 5 6 7 |
import MessageList from './messageList'; export default MessageList; export MessageItem from './messageItem'; export reducer from './reducer'; export actions from './actions'; |
好了,這樣外部的其他模組就可以這樣在他的 index.js 檔案中呼叫 message 模組了。
1 2 3 4 5 |
// bad import { reducer } from ./message/reducer; // good import { reducer } from ./message; |
收穫:按功能模組以及清晰的界限可以幫助我們很好的組織程式碼和目錄。
命名約定
在軟體程式設計中命名可真是一件令人頭疼的事情,這跟給孩子取名一樣費勁,哈哈。合適的命名是實現可維護性、易於理解的程式碼的最好實踐,React + Redux 的應用中提供了大量的約束來幫助我們組織程式碼,而且不會在命名上固執己見。無論你的函式封裝在 reducer 還是 component 中,在action creator 或是 selector 中,你都應該有一個命名約束,並且在擴充套件應用之前就確定如何命名,否則經常會讓我們陷入難以捉摸的回撥和重構當中。
而我習慣為每個型別的函式都加上一個字首。
- 在元件的callback中,為每個函式都加上 on 作為字首,比如 onCreateRplay
- 在改變 state 的 reducer 中加上 applay 作為字首,比如 applyCreateReply
- 在 selector 中 加上 get 作為字首,比如 getReply
- 在 action creator 中加上 do 作為字首,比如 doCreateReply
也許你不一定習慣這種加上字首的方式,不過我還是推薦給你,同時也建議找到自己喜歡的命名約束規則。
追蹤狀態的改變
在持續迭代中的應用免不了定義大量的 action,而且還需要追溯 state 是如何改變的,redux-logger 可以幫助你看到所有的 state change。每條日誌都會顯示出 previous state、執行的 action、next state。
不過你得確保 actions 是可被裝置的,因此我建議為不同型別的 action 都加上一個字首,比如這樣:
1 |
const MESSAGE_CREATE_REPLY = 'message/CREATE_REPLY'; |
這樣的話,無論你在何時觸發了資訊回覆這個動作,你都能看到 message/CREATE_REPLY
這一條日誌,如果出現 state 異常,便能迅速查到是那條錯誤的 state 改變而導致的。
儘可能讓 state tree 扁平化
在 Redux
中,扁平化的 state tree
可以讓你的 reducers
更加的簡單,這樣你就不需要在整個 store
的狀態樹中深層的查詢到某個 state 後再將其修改,而是可以很輕鬆的就能實現。不過,在 Redux 中卻不能做這麼做,因為 state 是不可變的。
如果你正在開發一個部落格應用,需要維護一個類似這樣的列表物件,列表中包含 author
和 comment
欄位:
1 2 3 4 5 6 |
{ post: { author: {}, comments: [], } } |
不過實際情況是每個物件都需要有對應的 id
來進行維護:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ post: { id: '1', author: { id: 'a', ... }, comments: [ { id: 'z', ... }, ... ], } } |
這個時候,我們將資料序列化之後將會變得更有意義,資料解構變得更加扁平化了。序列化之後的資料通過 id
關聯其他欄位,之後,你就可以通過實體物件來將其報酬,通過 id
來進行關聯資料的查詢。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
{ posts: { 1: { authorId: 'a', commentIds: ['z', ...] } }, authors: { a: { ... } }, comments: { z: { ... } }, } |
這樣,資料結構看起來就不在那麼深層巢狀了,當你需要改變資料的時候,就可以輕鬆的實現資料的不可變性了。
normalizr 是個強大的 library
,可以幫助我們進行資料格式化,噢耶~!
單一資料來源原則
格式化之後的資料可以幫助你按同步的方式來管理 state
,而假如請求後端介面後返回的是深層巢狀的 blog
的 posts
資料結構呢,是不是欲哭無淚啊?! post
欄位依然包含 author
和 comments
欄位,不過這次,comments
是一個陣列,陣列中的每個物件都有 author
欄位:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{ post: { author: { id: 'a' }, comments: [ { author: { id: 'b' }, reply: {}, }, { author: { id: 'a' }, reply: {}, }, ], } } |
我們可以看到資料結構中 author
欄位在 post
和 comments
中都有維護,這就導致巢狀的資料結構中出現了兩次,這就不是單一資料來源,當你改變了author
欄位的時候就會變得很困難了。
這個時候當你將資料格式化之後, author
這個欄位就只有一個了。
1 2 3 4 5 6 |
{ authors: { a: {}, b: {}, } } |
當你想 follow
一個 author
的時候,就可以輕鬆的更新一個欄位了 — 資料來源是單一的:
1 2 3 4 5 6 |
{ authors: { a: { isFollowed: true }, b: {}, } } |
應用中所有依賴了 author
這個欄位的地方都能得到更新。
Selectors
你還沒使用 selectors
嗎?沒關係,在 redux 中依然可以通過 mapStateToProps
來計算 props
:
1 2 3 4 5 |
function mapStateToProps(state) { return { isShown: state.list.length > 0, }; }; |
而如何你一旦使用了 selectors
之後的話,你就可以將這部分計算的工作放到 selectors
,從而讓 mapStateToProps
更加的簡潔:
1 2 3 4 5 |
function mapStateToProps(state) { return { isShown: getIsShown(state), }; }; |
你可以使用 reselect 來幫助你完成這些事情,它可以幫助你從 state
中計算得到衍生的資料,並且讓你的應用的效能得到提升:
Selectors
可以推匯出衍生資料,並傳遞所需資料的最小集,不用一次把所有資料都給元件,解決效能問題Selectors
是可組合的,它可以作為其他Selectors
的輸入Reselect
所提供的selector
是非常高效,除非它的引數改變了,否則selector
不會重新計算,這在複雜應用中對效能提升是非常有幫助的。
不斷的重構
隨著時間得推移,你會想要重構你的程式碼,無論是你在應用中使用了 React 、React + Redux 或者其他前端框架,你總會不斷的掌握更加高效的程式碼組織方式,或者是一些很好的設計模式。
如果你的應用中的元件非常的多,你可以找到一個更好的方式來分離和組織木偶元件和容器元件,你會發現他們之間的關係並做一些公共的抽取;如果你還沒有使用合適的命名約束,你也可以在重構的時候去做這些事情。
Generators, Sagas, Observables, Epics, …
Redux 是一個非常優秀的 library,讓我們可以體驗不同的程式設計正規化和技術。而大家又常常需要不構建不同的類庫來實現 async action
,這裡有幾種不同的方式來處理這些 side effects
:
- Redux Thunk – (Delayed) Functions
- Redux Promise – Promises
- Redux Saga – Generators
- Redux Observable – Observables
- Redux Loop – Elm Effects
新手的話建議使用 redux thunk 來處理一些非同步操作;等你慢慢的熟悉整個生態及其相關的應用的時候,可以看看其他的相關類庫。Redux Saga 是目前被廣泛採用的一種實現方式。不過,Redux Observables 目前也被越來越多的人所接受,這可是需要掌握不少關於 rxjs 及其響應式程式設計的概念及其使用方式。
其實,整體看來,redux 生態圈的本身就產生了非常多的前端類庫,真是讓人應接不暇啊。但也別煩惱,那些你不需要用到的東西,自然也不需要都去掌握,對吧。
多閱讀一下 Redux 的實現原始碼
Redux 本身的原始碼並不多,總共也才五六個關鍵檔案,不超千行程式碼。如果你想對 Redux 更加熟悉,那麼強烈建議你要抽些時間多分析一下他的原始碼。
在開始學習的時候,也推薦部分學習視訊給你:
- Redux 的作者 Dan Abramov 自己錄製的入門級視訊 《getting-started-with-redux》 ,大家都說錄製的很棒,不過說實話,這個對理解實現原理是很有幫助的。javascript-redux-implementing-store-from-scratch 和 javascript-redux-implementing-combinereducers-from-scratch 兩個視訊可以幫助你理解 store 和 combineReducer 的實現原理。
- 第二個系列的視訊是《building-react-applications-with-idiomatic-redux》,你可以從中學習到如何實現你自己的 middleware 中介軟體,學完後就可以學習如何在 store 中使用它們。然後,你就能掌握到如何使用 applayMiddleware 將中介軟體應用到 store 中
這些視訊內容不僅可以教你快速掌握如何使用 Redux,還可以讓你理解 Redux 的實現原理。最後,你就可以啃一啃 Redux 的原始碼了,可以學習到很多有意思的程式設計思想和函式式的運用。
編後語
本篇內容完結,更多內容請前往在 2017 年學習 React + Redux 的一些建議(下篇)。