一些問題
react為前端開發(fā)帶來了很多的便利,配合一些前端組件庫贡蓖,我們能夠十分迅速的開發(fā)出一個完整的前端項目。但是在使用react開發(fā)的過程中同樣會遇到很多問題,在個人的react開發(fā)過程中哀墓,認為主要集中在這幾個部分:
- 組件狀態(tài)管理
- 組件代碼和業(yè)務(wù)代碼強耦合
- 組件的粒度
組件狀態(tài)管理
組件狀態(tài)管理主要包括兩個部分:一個是組件的狀態(tài)存在哪里,另一個是組件的狀態(tài)該如何存儲喷兼。
對于第一個問題篮绰,其實比較好解答,對于一個復(fù)雜的react前端項目季惯,依賴于類似redux的flux流吠各,將組件狀態(tài)存儲在一個全局的store中是一個很好的選擇。在復(fù)雜前端項目的開發(fā)中勉抓,除了一些基礎(chǔ)公共組件贾漏,我會盡可能的避免將組件狀態(tài)存儲在組件state中。主要原因有兩點藕筋,一個是項目復(fù)雜后纵散,組件間的狀態(tài)傳遞會變得異常惡心,需要依靠大量props屬性傳遞函數(shù),并且強依賴組件生命周期函數(shù)伍掀。第二個從設(shè)計角度出發(fā)掰茶,react組件其實可以理解為一個純函數(shù),它根據(jù)傳遞進去的屬性做相應(yīng)的渲染硕盹,如果組件自己保持了state符匾,那么它可能會導(dǎo)致意料外的渲染結(jié)果。
第二個問題比較麻煩瘩例,對于一個組件啊胶,它的狀態(tài)可能包括兩個部分,一種可能是data垛贤,它們通常來自于server焰坪,比如一個列表list。另一種則是state聘惦,它表示的是頁面狀態(tài)某饰,比如一個table 的loading狀態(tài)。這兩種數(shù)據(jù)有時候甚至?xí)薪患埔铮虼巳绾喂芾硭鼈円彩且粋€問題黔漂。
組件代碼和業(yè)務(wù)代碼強耦合
react是一個負責(zé)view層的前端庫,所以組件本身是不包含業(yè)務(wù)邏輯的禀酱。抽象來說炬守,組件其實就是一個空的盒子,你給它什么剂跟,它就渲染出什么狀態(tài)减途,因此具體的業(yè)務(wù)邏輯不應(yīng)該在盒子里面做,而應(yīng)該抽象成具體給這個盒子什么樣的數(shù)據(jù)曹洽。因此在組件實現(xiàn)過程中鳍置,應(yīng)該避免參雜各種業(yè)務(wù)邏輯代碼,但這個其實不太好實現(xiàn)送淆,對于非redux項目税产,你可能在代碼中能夠看到各種setState方法調(diào)用,而在redux項目中偷崩,則是各種dispatch辟拷。
組件的粒度
在一個項目中,我們會寫很多組件环凿,有時候為了考慮組件的復(fù)用性梧兼,我們會把一個復(fù)雜的組件拆分成很多個基礎(chǔ)組件放吩≈翘基礎(chǔ)組件的實現(xiàn)其實要比業(yè)務(wù)組件復(fù)雜很多,因為服用的問題需要考慮更多的分支條件和邊界條件,對于開發(fā)速度會造成很大影響到推。
解決思路
其實對于上面的問題考赛,并沒有銀彈,也就是很難找到一個包治百病的方法莉测。更多的也是根據(jù)不同的業(yè)務(wù)場景做具體的實現(xiàn)颜骤,因此雖然這篇文章說的是最佳實踐,其實也只是對應(yīng)一些場景下的解決思路捣卤,但是應(yīng)該能夠解決大部分應(yīng)用場景下的痛點忍抽。
項目的整體框架如下,其實相對于其它react前端項目來說董朝,它最大的不同可能就是抽象出了一個業(yè)務(wù)層鸠项,下面我們具體介紹各個部分。
數(shù)據(jù)層
依賴于redux子姜,我們將所有的狀態(tài)存儲在一個全局的store中祟绊,所有對于store的修改都是通過action進行操作。在數(shù)據(jù)層中我們依賴了三個庫哥捕,分別是redux和redux-actions以及react-redux牧抽。redux應(yīng)該是目前使用最廣的flux庫,依賴redux我們可以很好的管理store中存儲的狀態(tài)遥赚。而依賴redux-actions扬舒,我們能夠快速的創(chuàng)建actions,并且在reducer中監(jiān)聽它們,react-redux可以通過connect方法將store中的數(shù)據(jù)映射到組件的props中鸽捻。
在store的設(shè)計上呼巴,我將它分成了兩部分,一塊是用來存儲來自server的數(shù)據(jù)data御蒲,另一部分則是組件本身的狀態(tài)state衣赶。這樣帶來的好處是可以解決某些狀態(tài)回退需求,比如我們有一個修改配置的操作厚满,如果只有state存儲配置數(shù)據(jù)府瞄,那么這次修改保存失敗后,我們無法退回到修改前的狀態(tài)碘箍。
配置的數(shù)據(jù)通常來自于遠端遵馆,因此原始數(shù)據(jù)存儲在data中,而這個彈窗表單的狀態(tài)則存儲在state中丰榴,這里存在一個問題货邓,就是我們需要講data中的數(shù)據(jù)拷貝一份到state中,同時保存成功后四濒,也需要對data中的數(shù)據(jù)做一下更新换况。對于數(shù)據(jù)的保存职辨,我們依賴immutable,關(guān)于immutable有許多文章介紹,這里就不做詳細介紹戈二。
#actions
import { createAction } from 'redux-actions';
export const setCount = createAction('SETCOUNT');
#reducer
import { handleActions } from 'redux-actions';
import Immutable from 'immutable';
export const test = handleActions({
SETCOUNT: (state, action) => state.setIn(['data', 'count'], action.payload),
}, Immutable.fromJS({data:{count: 0}}));
#View
import React from 'react';
import { connect } from 'react-redux';
const mapStateToProps = state => ({ count: state.getIn(['data', 'count'])});
const Test = (props) => {
return (<div>{props.count}</div>)
}
export default connect(mapStateToProps)(Test);
#這樣調(diào)用dispatch(setCount(10))就可以完成修改count的操作
業(yè)務(wù)層
在過去的項目中舒裤,具體的業(yè)務(wù)邏輯通常在View層中完成,因此我們能夠在view層的代碼中看到許許多多的dispatch方法觉吭,異步fetch方法腾供,以及數(shù)據(jù)組裝方法。這種情況其實是講業(yè)務(wù)邏輯和前端組件進行了強耦合鲜滩,造成了組件的復(fù)用性變差伴鳖,同時如果組件設(shè)計不合理,debug的時候問題的定位會變得異常困難徙硅,你往往分不清是組件出了問題黎侈,還是業(yè)務(wù)代碼出了問題。
為了解決這個問題闷游,在實踐過程中我們單獨的抽離了一層業(yè)務(wù)邏輯層峻汉,業(yè)務(wù)層中一個比較難處理的問題就是如何處理異步操作,這里我們主要依賴的是redux-thunk,這個組件其實很簡單脐往,代碼也很少休吠,感興趣的可以看看。它的主要作用是可以讓我們dispatch異步方法业簿,同時依賴react-redux中的bindActionCreators方法將業(yè)務(wù)層中的方法綁定到組件的props中瘤礁。
我們也將全局store也注入到了業(yè)務(wù)層代碼中,通過store.getState()方法和依賴reselect獲取狀態(tài)樹中的數(shù)據(jù)梅尤。而不依靠view組件中定義的方法傳遞的參數(shù)柜思。
通過上面的操作,前端組件和業(yè)務(wù)邏輯進行了充分的解耦巷燥。
#actions
import { createAction } from 'redux-actions';
export const setCount = createAction('SETCOUNT');
export const setServerData = createAction('SETSERVERDATA');
#reducer
import { handleActions } from 'redux-actions';
import Immutable from 'immutable';
export const test = handleActions({
SETCOUNT: (state, action) =>
state.setIn(['data', 'count'], action.payload),
SETSERVERDATA: (state, action) =>
state.setIn(['data', 'serverCount'], action.payload),
}, Immutable.fromJS({data:{count: 0, serverCount: 0}}));
#業(yè)務(wù)層actionCreators
import {setCount} from 'actions';
import store from 'reducer/store';
import { getCount } from 'selectors';
export const addOne() {
const count = getCount(store.getState());
dispatch(setCount(count + 1));
}
export const fetchDataFromServer() {
fetch('url')
.then(res => {
dispatch(setServerData(res))
})
.catch(e => { // error handle})
}
#業(yè)務(wù)層selectors
import { createSelector } from 'reselect';
export const data = state => state.test.get('data');
export const getCount = createSelector(
data,
data => data.get('count')
)
#View 變得簡單了
import React from 'react';
import { connect, } from 'react-redux';
import { bindActionCreators } from 'redux';
import { getCount } from 'selectors';
//將actionAreators中的方法綁定到view的props中
import * as actions from 'actionAreators'
const mapStateToProps = state => ({ count: getCount(state) });
const mapDispatchToProps =
dispatch => bindActionCreators({ ... actions }, dispatch);
const Test = (props) => {
addOne() {
this.props.addOne();
}
return (<div onClick={() => this.addOne}>{props.count}</div>)
}
export default connect(mapStateToProps赡盘,mapDispatchToProps)(Test);
#這樣調(diào)用dispatch(setCount(10))就可以完成修改count的操作
View層
對于前端組件我們將它分為三種,一種是基礎(chǔ)組件component缰揪,一種是業(yè)務(wù)組件widget陨享,以及頂層組件View。
對于基礎(chǔ)組件component钝腺,一般是粒度很細抛姑,跟業(yè)務(wù)沒有太大關(guān)系的組件。它們一般是公司自己的前端組件庫艳狐,或者一些開源組件庫定硝,以及項目中通用的組件『聊浚基礎(chǔ)前端組件的開發(fā)相對來說比較困難蔬啡,因為需要考慮很多邊界條件和分支條件唁毒,同時還需要提供比較完善的接口,特別是針對于一些有輸入屬性的組件更加需要注意星爪。
前端基礎(chǔ)組件的設(shè)計因為需要考慮到組件的復(fù)用性,會在組件中保留state粉私,以及使用setState方法顽腾,而不是像其他類型組件一樣完全依賴于傳入的屬性。這里推薦一下螞蟻的antd庫;
業(yè)務(wù)組件widget一般是由多個基礎(chǔ)組件組成的組件诺核,它們一般不保存自己的state抄肖,同時也不像View組件一樣從store中獲取數(shù)據(jù),而是完全通過傳入的props來進行渲染窖杀,同時它的事件handle函數(shù)也完全依賴于頂層View組件傳入漓摩。
View組件式頂層組件,它通過connect鏈接store中的數(shù)據(jù)和業(yè)務(wù)層中的方法入客,并將它們傳入到子組件的props中管毙,這樣View組件就相當(dāng)于整個頁面的入口,因此當(dāng)出現(xiàn)問題的時候桌硫,我們可以很好的定位夭咬,從View層一層層捋就行,而不用擔(dān)心是不是某個widget組件出了問題铆隘。
#Widget
import React from 'react';
export const ChildTest = (props) => {
click() {
this.props.clickHandle();
}
return (<div onClick={() => this.click}>{props.count}</div>)
}
#View
import React from 'react';
import { connect, } from 'react-redux';
import { bindActionCreators } from 'redux';
import { getCount } from 'selectors';
//將actionAreators中的方法綁定到view的props中
import * as actions from 'actionAreators'
import ChildTest from './widgets/ChildTest'
const mapStateToProps = state => ({ count: getCount(state) });
const mapDispatchToProps =
dispatch => bindActionCreators({ ... actions }, dispatch);
const Test = (props) => {
addOne() {
this.props.addOne();
}
return (<div><ChildTest clickHandle={() => this.addOne} /></div>)
}
export default connect(mapStateToProps卓舵,mapDispatchToProps)(Test);
這種設(shè)計思路下可能會造成的問題就是可能組件結(jié)構(gòu)會很深,不過這種情況可以在設(shè)計widget的時候解決膀钠,遇到很深的情況下我們最好將深層組件抽離出來掏湾,然后作為組件的props.children傳入。
其它
整個項目的結(jié)構(gòu)一般如下:
.
├── business
│ ├── actionCreators
│ └── selectors
├── common
│ ├── Constants.js
│ ├── Utils.js
│ └── apis
├── components
│ └── OptComponent.js
├── main.js
├── public
│ └── index.html
├── redux
│ ├── actions
│ ├── index.js
│ └── reducers
├── router.js
└── views
├── Index
└── Layout
存在的問題主要在于異步的處理肿嘲,后面發(fā)現(xiàn)邏輯復(fù)雜后融击,redux-thunk有一些力不從心■撸可能能使用其它換方案砚嘴,比如redux-saga。
寫在最后
這個框架實踐有一段時間了涩拙,大部分場景都能比較好的處理际长,但是對于一些復(fù)雜的場景也存在一些問題,比如大量的異步請求兴泥,異步競態(tài)出現(xiàn)的場景工育,這套框架可能就不太適合。
在整個實現(xiàn)過程中搓彻,其實很多思想都是借鑒了后端的如绸,其實業(yè)務(wù)層其實也就是后端的biz層嘱朽。數(shù)據(jù)層毋庸置疑對應(yīng)后端的數(shù)據(jù)層,只不過不是像mysql那樣的關(guān)系型數(shù)據(jù)庫怔接,而是mongodb或者redis這樣的非關(guān)系型數(shù)據(jù)庫搪泳,而action則是對應(yīng)的dao操作。
文章還有很多不足的地方扼脐,望斧正岸军,共勉~~