最近重構了一個項目,一個基於redux模型的react-native項目,目標是在混亂的代碼中梳理出一個清晰的結構來,爲了實現這個目標,首先須要對項目的結構作分層處理,將各個邏輯分離出來,這裏我是基於典型的MVC模型,那麼爲了將現有代碼重構爲理想的模型,我須要作如下幾步:javascript
這是一個老生常談的問題了,從16年起前端除了構建工具,討論的最多的就是組件化了,把視圖按照必定規則切分爲若干模塊過程就是組件化,那麼組件化的重點就是那個規則。前端
那麼這個規則又是什麼呢?java
按功能?按樣式?react
我以前的項目裏多數這兩種狀況都存在,舉個簡單的例子,對於app的登陸模塊來講就是一個典型的按功能分組,而對於一個列表就是一個明顯的按樣式去組件化,他們兩個對應着兩種徹底不一樣的寫法,由於他們一個是充血模型,一個是貧血模型。在redux中,明顯的區別是貧血組件中一切的狀態所有外置,組件自身不去管理本身的狀態,通通放到reducer;而在充血組件中,一部分狀態由全局的store去管理,一部分有自身的state控制。ajax
// 充血組件 // 貧血組件
組件A | 組件B | 組件C 組件A | 組件B | 組件C
邏輯A | 邏輯B | 邏輯C ---------------------
數據A | 數據B | 數據C 邏輯層
------------------- ---------------------
全局邏輯 數據層
複製代碼
在我重構的過程當中更傾向於將組件內的狀態都放在reducer中,這樣View就能夠更純粹的去渲染了,這樣的View在我看來會更加簡潔、更加清晰,對於組件的替換更是得心應手。但狀態全外置這種實踐帶來的代價也是很大的。由於一個帶交互的組件,勢必須要一些事件的處理,生命週期的觸發等等操做,這會帶來一些問題:數據庫
對於後一點我認爲並無很大的問題,得益於分層和純渲染的設計,組件將控制自身的行爲交出後能夠將這些邏輯抽象爲更加通用的邏輯,從而方便有相似需求的組件使用,由於邏輯應該只出如今一個地方,而不該分散在多個地方。例如控制一批組件的顯示或隱藏,將組件內部控制顯示的邏輯交出來反而會省去更多的重複代碼。redux
而我更擔憂的是因爲組件中私有狀態的轉移致使的Store膨脹的問題,爲了不這個問題首先作的即是儘量的提取公用有類似做用的狀態,例如控制顯示/隱藏、多個列表的頁數/條數;等這些有着類似功能的字段。走到這一步就引出了另一個問題了,對於組件的狀態描述是樹形的仍是平行的。後端
這種結構的特色是將一個組件的狀態經過一個樹的形式記錄下來,頁面是如何嵌套的,那麼狀態樹就是如何嵌套的,這樣作的好處是組件接收到狀態後直接遞歸的顯示就好了,對於組件來講這是最簡單,效率最高的展示形式。但這樣作的問題就是若是有多個類似的組件就會形成Store中冗餘大量重複數據,最終形成Store的膨脹。react-native
這種結構和上面的樹形結構偏偏相反,能夠最大程度的避免冗餘數據的產生,將每一類數據拍平保存,但這種形式對於組件的展現卻很不友好,組件須要本身去消化多處數據源帶來的格式化操做,在redux中connect方法就是用來處理這種多數據源聚合用的。數組
那麼上面兩種結構改如何取捨呢?我我的推薦第二種平行結構,既然選擇了平行結構,那麼該如何去處理數據聚合的問題呢?在這裏我推薦利用管道的思路來解決,這借鑑了 Angular 2 Pipe的概念,固然熟悉Linux的同窗對於|
操做符必定也不會陌生。在咱們的項目中,數據是流動的,如同一個管道中的水同樣,Store就是一個水庫,聚集了各類各樣的數據(水),而頁面組件就如同須要灌溉的田,而從水庫到田間這段距離就須要水管的幫助了。一樣的,利用pipe咱們能夠將保存在Store中的數據轉換成指望看到的結構,而這一切操做都是在數據的流動中完成的,而不是放在數據已經傳遞到組件以後去處理了。
這裏引出了一個概念,就是數據流這個概念,在項目中我將全部數據的操做都成爲數據的流動。舉個例子,當用戶在登陸框輸入了用戶名和密碼並點擊提交以後,這兩個input中的value就變成了兩個數據流:
input => merge(name, password) => filter(校驗合法性) => post(服務器)
複製代碼
這個行爲變成了一條流水線,先無論post輸出的結果如何,在上面的demo中咱們的輸入行爲被抽象成了兩個參數,最後經過合併、過濾、發送,最終到達服務器,這不是一個新概念,在不少的框架中都有體現:
在Cycle.js它被稱爲 Intent(負責從外部的輸入中,提取出所需信息),Intent實際上作的是action執行過程的高級抽象,提取了必要的信息。因爲View是純展現的,因此包括事件監聽在內的行爲通通被Intent抽象成數據源,這在RxJs中很常見:
var clicks = Rx.Observable.fromEvent(document, 'click');
clicks.subscribe(x => console.log(x));
// 結果:
// 每次點擊 document 時,都會在控制檯上輸出 MouseEvent 。
複製代碼
相比於從View中發出的同步數據源,咱們遇到更多的是從HTTP中獲取的異步數據源。在redux中咱們經常使用redux-thunk來處理異步操做,那麼在流中呢?
在以前的業務中咱們有不少方式去處理異步操做,好比說最經常使用的redux-thunk(回調)、promise、async/await。如今不少人更願意用async/await操做符去寫異步邏輯,由於它讓代碼顯得更加「同步」,我以前也很喜歡這種方式,但如今在數據流的概念中,同步/異步已經被「模糊」了,它們都是數據源,它們都是「主動」發出數據的,那麼同步仍是異步就顯得不那麼重要了,仍是上面的例子,若是用戶名變成了一個異步獲取的過程,而不是用戶主動輸入的了:
input => merge(async(name), password) => filter(校驗合法性) => post(服務器)
複製代碼
這種狀況下在RxJs中能夠經過zip
來等待所有的數據流
let age$ = Observable.of<number>(27, 25, 29);
let name$ = Observable.of<string>('Foo', 'Bar', 'Beer');
let isDev$ = Observable.of<boolean>(true, true, false);
Observable
.zip(age$,
name$,
isDev$,
(age: number, name: string, isDev: boolean) => ({ age, name, isDev }))
.subscribe(x => console.log(x));
// 輸出:
// { age: 27, name: 'Foo', isDev: true }
// { age: 25, name: 'Bar', isDev: true }
// { age: 29, name: 'Beer', isDev: false }
複製代碼
經過這樣的鏈式操做,咱們能夠很方便的控制和獲取數據流,這是對於數據的獲取,那麼數據的分發呢?在redux中,咱們一般會屢次dispatch,在redux-thunk中咱們會這樣寫:
const getInfo = (params) => async (dispatch, getState) => {
// TODO...
dispatch(actionaA);
// TODO...
dispatch(actionaA);
}
複製代碼
而在redux-observable中:
const somethingEpic = (action$, store) =>
action$.ofType(SOMETHING)
.switchMap(() =>
ajax('/something')
.do(() => store.dispatch({ type: SOMETHING_ELSE }))
.map(response => ({ type: SUCCESS, response }))
);
複製代碼
可是我認爲處處dispatch是一個很差的行爲,這會讓一個流變得混亂,由於你在流的最後不會得完整的結果(在過程當中有一部分就已經派發出去了),這會讓邏輯看起來很散亂,因此我推薦應該寫成這樣的形式:
const somethingEpic = action$ =>
action$.ofType(SOMETHING)
.switchMap(() =>
ajax('/something')
.mergeMap(response => Observable.of(
{ type: SOMETHING_ELSE },
{ type: SUCCESS, response }
))
);
// 上面這兩段demo來着redux-observable的文檔
複製代碼
結束了異步的處理,咱們的流模型也完成了input->output的完整閉環了。在這裏沒有詳細說output是由於基於redux,我任然是經過redux的connect方法將Store分發注入到組件的props中去的,所以若是你熟悉redux那麼會很習慣如今的改變。
在處理完了同步/異步以後咱們就來聊聊業務的邏輯該如何處理了。在redux中邏輯被分在了兩個地方,action和reducer中,一個是作數據的聚合,一個是作數據的格式化。上面提到了Intent 是action的高階抽象,實際上是對action的拆分,剝離了action中獲取數據的部分邏輯,那麼剩下的就是數據處理的部分了,這部分在個人實踐中被叫作Service。
這是一個單例的實例,整個項目中一個服務只會有一個實例,沒必要將相同的代碼複製一遍又一遍,只須要建立一個單一的可複用的數據服務,而且把它注入到須要它的那些組件中。而且使用單獨的服務能夠保持組件足夠的精簡,同時也更容易對組件進行單元測試。一樣reducer中的數據格式化邏輯也遷到了服務中去處理,在redux中reducer兼顧着數據的格式化和數據的保存這兩個功能,如今咱們將完全剝離出數據的處理部分,剩下的reducer將只作數據的保存,這就又引出了另外一個概念Model,這一層咱們一會討論,接着業務處理來看,在數據流獲取到數據並處理分發到Model中以後,input這一步基本算是結束了,接下來就是由Model到View的output了。
上文中我說道了我推薦使用平行模式,那麼在平行模式到View這種樹型結構該若是轉化呢?這是output中最重要的一步,在CycleJS中這一步一般由filter去完成,而在Angular中則是由Pipe去處理,不管它叫什麼,它們都是這條流程上的一環,就像水管中的一節同樣,全部從Model通向View的數據都會進過這一環,從而被格式化。在代碼中我更推薦你們嘗試使用Decorator去過濾數據源:
@UserInfoPipe({ name: 'Model.UserInfo.name' })
class LoginDemo extends Component {
constructor(props) {
super(props);
}
render(){
return (
<View>
<Text>{this.props.name}</Text>
</View>
);
}
}
複製代碼
如今總體的骨架已經有了,剩下的就是該如何更好的抽象整合項目中的數據了。
最一開始的項目因爲爲了方便,我就按照API的結構去設計Store,那個時候一個頁面對應一個接口或者不多的幾個接口,這時候我將API返回的結構與本地的狀態一一對應,這在初期很是的方便,不須要我作過多的轉換,然而接下來爲了應付接口的各類異常,不得不寫不少防護性的代碼(字段判空、屬性變動、接口數據拼裝),最後這些代碼變得臃腫不堪,在其它同窗介入修改的時候老是一頭霧水,老是改了這裏,那裏出又出了問題。而且這其中也存在很多冗餘的數據。
後來我發現既然數據都是最終給View去用的,那麼我就按View的需求去設計Store好了,這個Store對於展現的組件來講,使用起來很是方便,當前應用處於哪一種狀態,就用對應狀態的數組類型的數據渲染,不用作任何的中間數據轉換。不過這也一樣形成數據冗餘的問題,而且若是我須要改動頁面的某個字段的話,須要在不少地方去修改,由於這個Store樹變得很深枝葉不少。
那麼我如今該如何設計狀態呢?做爲一個曾經作過一段時間後端的我來講,我決定模仿數據庫的結構去設計狀態樹。把Store當成一個數據庫,每一個種類的狀態看作數據庫中的一張表,狀態中的每個字段對應表的一個字段。
那麼設計一個數據庫,應該要遵循哪些原則呢?
而基於上面這三條原則,咱們怎麼設計Store呢?
怎麼理解這件事呢?舉個例子,我有一個長列表,每當我點擊列表中的某一列時就會有一個紅框出現包裹住這列,而這個列表中真正展現的數據應該是另一個子狀態,它們的關係相似:
{
activeLine: 1,
list: [
{
name: 'test1',
},
{
name: 'test2',
},
{
name: 'test3',
},
{
name: 'test4',
},
]
}
複製代碼
有了惟一的key作主鍵,咱們就能夠很方便的去遍歷/處理數據。更進一步的,若是咱們想去判斷一條數據有沒有變化,咱們能夠單純的去判斷主鍵是否一致,在一些狀況下,這是一個不錯的思路,這避免了多層判斷,或者深拷貝帶來的複雜度和性能問題(這個能夠參考immutable)。
什麼是原子數據?頁面中使用到的數據都是由這些原子數據經過計算、拼裝獲得的(注意:這裏只有拼裝,沒有拆分,由於原子是最小的單位,因此是不可拆分的);這就保持了數據源的統一,不會出現一份同樣的數據來自多出數據源的問題了,這會避免不少沒必要要的問題,如多處數據源不一樣步致使的頁面展現異常等問題。
好了,數據層也設計完了,這樣一個完整的結構就清晰的擺在面前了,最終總結一下這個過程:
通過以上幾步,咱們就初步的完成了一個業務從input到output的完整閉環。
已上這些即是我此次重構總結的一些經驗,確定不全對、不完善、不許確,可是這個大方向我以爲是值得去探索的。