首发于pure render
React实践 - Component Generator

React实践 - Component Generator

我们开发一个新产品的时候,通常会先抽象出一些公用的组件,然后通过这些组件来拼装成页面。不知道大家有没有发现,这种开发方式带来的问题是一个团队内经常会有这样的场景:

A 已经开发了一个 XX 表格模块,B 要开发一个类似的 YY 表格模块,然后 B 通常是去把 A 的代码 copy 一下,修改一些东西;或者不巧 B 不知道 A 已经开发 XX 表格,然后 B 又得一行行的写一些类似的代码。

造成这种问题的原因简单的说就是:组件抽象的粒度太单一。接下来我们会通过两个例子来讲述问题及我们如何解决这样的问题的。

一个简单的组件 - Switch

首先我们看一个简单的 Switch 组件,如果一个产品中有常用的两种切换功能:

如果使用之前封装的基础组件组件 Switch 来实现,我们需要如下调用:

<Switch
 className="switch"
 activeIndex={this.state.activeIndex}
 onChange={::this.handleSwitchChange}
>
 <SwitchItem>趋势</SwitchItem>
 <SwitchItem>列表</SwitchItem>
</Switch>

这种组件抽象方式(实现省略)好处就是通用性强,但带来一些问题:

  • 每个人都需要维护选项的展示名称和顺序之间的关系

  • 调用代码较长,有冗余

于是,我们对这类组件进行了重构,希望让每个组件使用更加简单,只需要关系具体的状态即可。具体的做法就是开发一个 Generator—— generateSwitch 来生成常用的切换组件:

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' },
]);

这种做法就可以解决上面说的问题:

  • 比常见的切换组件使用更加便利,调用代码一行就够了,而且能够起到统一参数的作用;

  • 对外暴露生成函数 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 来配置,所以我们也需要根据具体的业务来把握是否要进行抽象。

编辑于 2016-06-21 09:46