咱们开发一个新产品的时候,一般会先抽象出一些公用的组件,而后经过这些组件来拼装成页面。不知道你们有没有发现,这种开发方式带来的问题是一个团队内常常会有这样的场景: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 来配置,因此咱们也须要根据具体的业务来把握是否要进行抽象。