React 实践:改造 Redux action generator,编写 Redux reducer generator (1)

Author Avatar
Splendour 1月 25, 2017

背景

从几个月前开始,写了一篇博文 编写自己项目的 Redux action generator,并且在项目实践中用上了,大大减少了编写 action 时的代码量,提高了开发效率。可是对于 reducer,还是需要手写很多东西,这其中也有很多类似的代码,因此,在上个项目告一段落后,开始构思如何通过类似的方式,用配置生成 reducer

原有的写法

以项目中一个用户模块为例,它的 reducer 如下:

export default combineReducers({
  list: (state = [], action) => {
    switch (action.type) {
      case getAction(NAME, 'list', defines.ACTION_STATUS_LOAD):
        return [];
      case getAction(NAME, 'list', defines.ACTION_STATUS_SUCCESS):
        return Object.assign([], state, action.response.data.tenantAdmins);
      default:
        return state;
    }
  },
  listLoading: (state = false, action) => {
    switch (action.type) {
      case getAction(NAME, 'list', defines.ACTION_STATUS_LOAD):
        return true;
      case getAction(NAME, 'list', defines.ACTION_STATUS_SUCCESS):
      case getAction(NAME, 'list', defines.ACTION_STATUS_FAIL):
        return false;
      default:
        return state;
    }
  },
  ···

其中,getAction 方法是获取全局唯一的 action 值,此处不细说。

思路

可以看到,listlistLoading 是很常见的 reducer,几乎在所有列表页面都会出现,而且是伴随着 list 这个 action 出现的。仔细去回忆不难发现,我们所有定义的 action,基本都会需要 reducer 与之配合使用。那么,我们就可以从 action 的配置出发,来生成一些通用的 reducer

异步 action

这里指的是需要请求服务器获取数据的异步操作,我们原有的 action 是这么写的

{
  name: 'list',
  isAjax: true,
  path: 'apis/notice',
  method: 'GET',
  msgType: defines.AJAX_TYPE_LOAD
}

根据需求的不同,我们在模块中可能会添加这些 reducer

  • listData
  • listLoading
  • listCurPage
  • listTotal
  • listFilter

其中,listCurPagelistFilter 需要额外的 action 与之配合使用。所以,我们添加以下配置,让异步 action 的配置能够自动生成我们想要的 actionreducer

  • data: {initialState, nextState(state, action)}

    • 这个配置代表这个 action 需要在其请求成功后存储数据到 reducer
    • 约定生成的 reducer 名称为 ${name}Data
    • initialState 配置该 reducer 的初始值
    • nextState(state, action) 是一个函数,配置触发 getAction(prefix, name, defines.ACTION_STATUS_SUCCESS)reducer 的响应
    • 代码
      result[`${name}Data`] = (state = data.initialState, action) => {
      if (action.type === getAction(prefix, name, defines.ACTION_STATUS_SUCCESS)) {
        return data.nextState(state, action);
      }
      return state;
      };
      
      (其中,result 是最终传给 combineReducers 的一个对象,下文同理)
  • loading: true

    • 这个配置代表这个 action 需要一个与之对应的 loading 状态标识
    • 约定生成的 reducer 名称为 ${name}Loading
    • 自动配置触发 getAction(prefix, name, defines.ACTION_STATUS_LOAD) 时返回 true, 触发 getAction(prefix, name, defines.ACTION_STATUS_SUCCESS)getAction(prefix, name, defines.ACTION_STATUS_FAIL) 时返回 false
    • 代码
      result[`${name}Loading`] = (state = false, action) => {
      if (action.type === getAction(prefix, name, defines.ACTION_STATUS_LOAD)) {
        return true;
      }
      if (action.type === getAction(prefix, name, defines.ACTION_STATUS_SUCCESS)) {
        return false;
      }
      if (action.type === getAction(prefix, name, defines.ACTION_STATUS_FAIL)) {
        return false;
      }
      return state;
      };
      
  • pagination: true

    • 这个配置代表这个 action 需要分页获取数据
    • 约定生成 ${name}CurPage${name}Total 两个 reducer
    • 自动生成新的 action: set${toCamel(name)}Page (toCamel 函数是将传入的 name 转化为首字母大写的形式,为了符合整个驼峰式的要求,下文同理)
    • 约定该请求返回的数据中,要带上 count 代表数据的总量,在触发 getAction(prefix, name, defines.ACTION_STATUS_SUCCESS) 时,将该值设置到 ${name}Total 中去
    • 调用 set${toCamel(name)}Page 时,${name}CurPage 返回 action.data
    • action_generator 代码

      Object.assign(result, {
      [getAction(actionPrefix, `set${toCamel(name)}Page`)]:
        getAction(actionPrefix, `set${toCamel(name)}Page`),
      
      [`set${toCamel(name)}Page`]: data => ({
        type: getAction(actionPrefix, `set${toCamel(name)}Page`),
        data
      })
      });
      

      (其中,result 是最终的 action 文件 export 出去的对象,包含所有的 action,下文同理)

    • reducer_generator 代码
      result[`${name}Total`] = (state = 0, action) => {
      if (action.type === getAction(prefix, name, defines.ACTION_STATUS_SUCCESS)) {
        return action.response.data.count;
      }
      return state;
      };
      result[`${action.name}CurPage`] = (state = 1, action) => {
      if (action.type === getAction(prefix, `set${toCamel(name)}Page`)) {
        return action.data;
      }
      return state;
      };
      
  • filter: true

    • 这个配置代表这个 action 需要筛选功能,在发出请求时带上筛选条件
    • 约定生成的 reducer 名称为 ${name}Filter
    • 自动生成 set${toCamel(name)}Filterclear${toCamel(name)}Filter 两个 action
    • 约定 filter 为一个 Object,初始值为 {}
    • 调用 set${toCamel(name)}Filter 时,${name}Filter 返回 action.data
    • 调用 clear${toCamel(name)}Filter 时,${name}Filter 返回 {}
    • action_generator 代码

      Object.assign(result, {
      [getAction(actionPrefix, `set${toCamel(name)}Filter`)]:
        getAction(actionPrefix, `set${toCamel(name)}Filter`),
      
      [`set${toCamel(name)}Filter`]: data => ({
        type: getAction(actionPrefix, `set${toCamel(name)}Filter`),
        data
      })
      });
      Object.assign(result, {
      [getAction(actionPrefix, `clear${toCamel(name)}Filter`)]:
        getAction(actionPrefix, `clear${toCamel(name)}Filter`),
      
      [`clear${toCamel(name)}Filter`]: data => ({
        type: getAction(actionPrefix, `clear${toCamel(name)}Filter`),
        data
      })
      });
      
    • reducer_generator 代码
      result[`${action.name}Filter`] = (state = {}, action) => {
      if (action.type === getAction(prefix, `set${toCamel(name)}Filter`)) {
        return action.data;
      }
      if (action.type === getAction(prefix, `clear${toCamel(name)}Filter`)) {
        return {};
      }
      return state;
      };
      

弹框类或切换显示类 action

这里指的是 action 需要控制某些组件的显示或隐藏,我们原有的 action 是这么写的

{
  name: 'showForm'
},
{
  name: 'hideForm'
},
{
  name: 'toggleForm'
}

可以看到,这里需要写两个或三个 action,并且需要在 reducer 中去手动编写对应关系

formShow: (state = false, action) => {
  switch (action.type) {
    case getAction(NAME, 'showForm'):
      return true;
    case getAction(NAME, 'hideForm'):
      return false;
    case getAction(NAME, 'toggleForm'):
      return !state;
    default:
      return state;
  }
}

我们将其视为一种类型的 action

  • 约定生成 show${toCamel(name)} hide${toCamel(name)} toggle${toCamel(name)} 三个 action 用来控制
  • 约定生成的 reducer 为 ${name}Show
  • action_generator 代码

    Object.assign(result, {
    [getAction(actionPrefix, `show${toCamel(name)}`)]:
      getAction(actionPrefix, `show${toCamel(name)}`),
    
    [`show${toCamel(name)}`]: data => ({
      type: getAction(actionPrefix, `show${toCamel(name)}`),
      data
    })
    });
    Object.assign(result, {
    [getAction(actionPrefix, `hide${toCamel(name)}`)]:
      getAction(actionPrefix, `hide${toCamel(name)}`),
    
    [`hide${toCamel(name)}`]: data => ({
      type: getAction(actionPrefix, `hide${toCamel(name)}`),
      data
    })
    });
    Object.assign(result, {
    [getAction(actionPrefix, `toggle${toCamel(name)}`)]:
      getAction(actionPrefix, `toggle${toCamel(name)}`),
    
    [`toggle${toCamel(name)}`]: data => ({
      type: getAction(actionPrefix, `toggle${toCamel(name)}`),
      data
    })
    });
    
  • reducer_generator 代码
    result[`${name}Show`] = (state = false, action) => {
    if (action.type === getAction(prefix, `show${toCamel(name)}`)) {
      return true;
    }
    if (action.type === getAction(prefix, `hide${toCamel(name)}`)) {
      return false;
    }
    if (action.type === getAction(prefix, `toggle${toCamel(name)}`)) {
      return !state;
    }
    return getExtraMapper(prefix, `${name}Show`, state, action, extras);
    };
    

设置类 action

这里指的是 action 需要设置某些值到 reducer 中,我们原有的 action 是这么写的

{
  name: 'setSelected'
},
{
  name: 'clearSelected'
}

可以看到,这里需要写两个 action,并且需要在 reducer 中去手动编写对应关系

selected: (state = '', action) => {
  switch (action.type) {
    case getAction(NAME, 'setSelected'):
      return action.data;
    case getAction(NAME, 'clearSelected'):
      return '';
    default:
      return state;
  }
}

我们将其视为一种类型的 action

  • 约定生成 set${toCamel(name)} clear${toCamel(name)} 两个 action
  • 约定生成的 reducer 为 ${name}Show
  • action_generator 代码

    Object.assign(result, {
    [getAction(actionPrefix, `set${toCamel(name)}`)]:
      getAction(actionPrefix, `set${toCamel(name)}`),
    
    [`set${toCamel(name)}`]: data => ({
      type: getAction(actionPrefix, `set${toCamel(name)}`),
      data
    })
    });
    Object.assign(result, {
    [getAction(actionPrefix, `clear${toCamel(name)}`)]:
      getAction(actionPrefix, `clear${toCamel(name)}`),
    
    [`clear${toCamel(name)}`]: data => ({
      type: getAction(actionPrefix, `clear${toCamel(name)}`),
      data
    })
    });
    
  • reducer_generator 代码
    result[`${name}`] = (state = initialState, action) => {
    if (action.type === getAction(prefix, `set${toCamel(name)}`)) {
      return action.data;
    }
    if (action.type === getAction(prefix, `clear${toCamel(name)}`)) {
      return initialState;
    }
    return getExtraMapper(prefix, `${name}`, state, action, extras);
    };
    

总结

以上就是通过 action 的配置来生成一些常用的 reducer 和 额外的 action 的做法,那不通过 action ,直接通过配置来生成 reducer 需要怎么做呢?想要往自动生成的 reducer 中增加自定义的转换(action 触发 reducer 变化)应该怎么做呢?下一篇博文我们再来一同探究。