咱們開發一個新產品的時候,一般會先抽象出一些公用的組件,而後經過這些組件來拼裝成頁面。不知道你們有沒有發現,這種開發方式帶來的問題是一個團隊內常常會有這樣的場景:javascript
A 已經開發了一個 XX 表格模塊,B 要開發一個相似的 YY 表格模塊,而後 B 一般是去把 A 的代碼 copy 一下,修改一些東西;或者不巧 B 不知道 A 已經開發 XX 表格,而後 B 又得一行行的寫一些相似的代碼。java
形成這種問題的緣由簡單的說就是:組件抽象的粒度太單一。接下來咱們會經過兩個例子來說述問題及咱們如何解決這樣的問題的。函數
首先咱們看一個簡單的 Switch
組件,若是一個產品中有經常使用的兩種切換功能:ui
若是使用以前封裝的基礎組件組件 Switch
來實現,咱們須要以下調用:this
<Switch className="switch" activeIndex={this.state.activeIndex} onChange={::this.handleSwitchChange} > <SwitchItem>趨勢</SwitchItem> <SwitchItem>列表</SwitchItem> </Switch>
這種組件抽象方式(實現省略)好處就是通用性強,但帶來一些問題:url
每一個人都須要維護選項的展現名稱和順序之間的關係spa
調用代碼較長,有冗餘code
因而,咱們對這類組件進行了重構,但願讓每一個組件使用更加簡單,只須要關係具體的狀態便可。具體的作法就是開發一個 Generator —— generateSwitch
來生成經常使用的切換組件:blog
export const generateSwitch = (name, options) => { const propTypes = { className: PropTypes.string, activeKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), onChange: PropTypes.func.isRequired, }; const Switch = (props) => { ... return ( <span className={classes}> { options.map((entry, index) => ( ... )) } </span> ); }; Switch.propTypes = propTypes; Switch.displayName = name; return Switch; }; export const ASwitch = generateSwitch('ABSwitch', [ { name: 'AA', key: 'a' }, { name: 'BB', key: 'b' }, ]); export const BSwitch = generateSwitch('CDSwitch', [ { name: 'CC', key: 'c' }, { name: 'DD', key: 'd' }, ]);
這種作法就能夠解決上面說的問題:ip
比常見的切換組件使用更加便利,調用代碼一行就夠了,並且可以起到統一參數的做用;
對外暴露生成函數 generateSwitch
也能保證通用性。
下面以一個表格業務爲例,常見的表格模塊以下:
在開發這個模塊的時候,雖然每一個小區塊咱們都抽取了相應的組件,如 Selector
, Table
, Switch
, Pagination
等。可是把這些組件串起來也有不少邏輯,若是每一個相似的模塊都重複寫,任何一個小的邏輯發生變化,均可能須要修改全部的模塊實現。因此這時候咱們想作的事情就是:這個模塊自己也是一個組件,咱們只須要經過一些配置生成不一樣的模塊,而不是重複的 copy 代碼,而後修改一些差別的地方。
在這裏碰到的一個問題是,咱們整個系統是使用 Redux 來管理數據的,整個項目的 Store
結構以下:
每一個業務模塊會去 connect
相應的數據以及 actions
,每一個模塊都有相應的 reducer
。而且每一個卡片的 action
也須要作到全局惟一,因此咱們給模塊的 UI Component 以及 reducer 分別開發了相應的 Generator。
首先來看 UI Component 的 Generator:
function generateAbcModule({pageName, moduleName}) { const ACTION_PREFIX = `${pageName}/${moduleName}`; const LOAD = ACTION_PREFIX + 'LOAD'; ... function load(url, params, id) { return (dispatch, getState) => { const state = getState(); ... return dispatch({ type: LOAD, .... }); }; } @connect((state, props) => { const moduleState = state[pageName][moduleName]; return { ...moduleState, }; }, { load, }) class AbcModule extends Component { ... } return AbcModule; }
經過代碼發現,咱們把 actionCreators
與 UI 放在了一塊兒,而後經過 pageName
和 moduleName
來惟一地標識一個模塊,拼裝這兩個參數做爲 action
的前綴,從而達到每一個模塊的 action
是全局惟一的。
接下來咱們是 reducer
的 Generator:
function generateAbcModuleReducer({pageName, moduleName, defaultIndexes}) { const ACTION_PREFIX = `${pageName}/${moduleName}/`; const LOAD = ACTION_PREFIX + 'LOAD'; const initialState = { indexes: defaultIndexes, ... }; return function AbcModuleReducer(state = initialState, action) { switch (action.type) { case LOAD: return { ...state, isLoading: true, ... }; ... } };
相似的,reducer Generator 也是經過 pageName
和 moduleName
來惟一地標識一個模塊。固然每一個模塊可能會有不一樣的 initialState
,這個也能夠經過 generateAbcModuleReducer 的入參來設置。
上面這種使用 Generator 來封裝業務模塊的方法,可以在必定程度上減小重複代碼,加快開發速度,不過若是業務發展的很快,有可能會致使業務模塊組件 props 氾濫 的問題。
以上面的排行卡片爲例,可變的東西就很是多,相應的就須要不少的 props 來配置,因此咱們也須要根據具體的業務來把握是否要進行抽象。