React 实践:编写自己项目的 Redux action generator
背景
使用 React + Redux 也有一段时间了,从第一次尝试使用之后就开始按一种方式写代码,思路很清晰,但就是有一个问题,感觉每写一个 action 都是在 copy + paste,然后修改其中的部分内容,最近想优化代码所以尝试着编写一个 generator 来生成这些 actions,让代码更加优雅。
原有的写法
这是一个 action 的部分代码
export const USER_LIST_LOAD = 'USER_LIST_LOAD';
export const USER_LIST_SUCCESS = 'USER_LIST_SUCCESS';
export const USER_LIST_FAIL = 'USER_LIST_FAIL';
exports.loadUserList = (tenantId, sucCallback, errCallback) =>
  (dispatch, getState) =>
  dispatch({
    [CALL_API]: {
      types: [
        USER_LIST_LOAD,
        USER_LIST_SUCCESS,
        USER_LIST_FAIL
      ],
      endpoint: `apis/${tenantId}/user`,
      schema: Schemas.NORMAL_RESP,
      method: 'GET',
      errMsg: '获取数据失败,请刷新重试',
      sucCallback,
      errCallback
    }
  });
export const USER_LIST_SELECT = 'USER_LIST_SELECT';
exports.userListSelect = keys => ({
  type: USER_LIST_SELECT,
  keys
});
思路
- 每一个 
action无非就是 export 出在全局唯一的常量作为 action 的标识,并且提供可以调用的方法来 dispatch 这样的常量。这里的 action 分为两类,ajax 请求类和非 ajax 请求类,ajax 请求类会 export 3 个常量,分别代表 请求中,请求成功以及请求失败 3 个状态。 - 传进来的参数,应该要统一成一个(ajax 请求的数据都放在第一个参数里,第二个和第三个固定为成功回调和失败回调)。
 - 为了保证 action 的唯一性,我们需要传入一个参数作为前缀,一般用页面名作为前缀(这里是 USER)。
 - 为了用配置的方式写 actions,考虑把 actions 用 json 数组的方式传进来,这样可以用到 map 函数来遍历并且生成每一个 action。
- 最后要将所有的 action 组装成一个变量 export 出去,再对原有的 import 方法进行修改就可以了。 
generator 代码
首先,根据是否 ajax 请求,生成对应的 action
const actionList = (actionPrefix, actions) =>
  map(action =>
    action.isAjax ? ({
      [`${actionPrefix}_${action.name.toUpperCase()}_LOAD`]:
        `${actionPrefix}_${action.name.toUpperCase()}_LOAD`,
      [`${actionPrefix}_${action.name.toUpperCase()}_SUCCESS`]:
        `${actionPrefix}_${action.name.toUpperCase()}_SUCCESS`,
      [`${actionPrefix}_${action.name.toUpperCase()}_FAIL`]:
        `${actionPrefix}_${action.name.toUpperCase()}_FAIL`,
      [action.name]: (data, sucCallback, errCallback) =>
        (dispatch, getState) =>
          dispatch({
            [CALL_API]: {
              types: [
                `${actionPrefix}_${action.name.toUpperCase()}_LOAD`,
                `${actionPrefix}_${action.name.toUpperCase()}_SUCCESS`,
                `${actionPrefix}_${action.name.toUpperCase()}_FAIL`
              ],
              endpoint: action.path,
              schema: action.schema || Schemas.NORMAL_RESP,
              method: action.method,
              errMsg: action.errMsg || '获取数据失败,请刷新重试',
              data,
              sucCallback,
              errCallback
            }
          })
    }) : ({
      [`${actionPrefix}_${action.name}`]:
        `${actionPrefix}_${action.name}`,
      [action.name]: data => ({
        type: `${actionPrefix}_${action.name}`,
        data
      })
    })
  )(actions);
- 传进来的参数有两个,一个 
actionPrefix,一个包含每个 action 信息的数组 - 对每个 action 来说,
isAjax用来区分是否 ajax 请求;name是 action 的名字,需要是唯一的;path和method分别代表 ajax 请求的地址和方法(get、post 等) - 这里的 
map函数来自于ramda库 
然后,将上面生成的 action 数组合并起来并 export 出去
export default (actionPrefix, actions) => {
  const result = {};
  forEach(action =>
    Object.assign(result, action)
  )(actionList(actionPrefix, actions));
  return result;
};
- 这里定义了该文件 export 一个函数,接收两个参数
 - 这个函数能够先通过传进来的 actions 生成一系列方法和变量,然后组装起来
 - 这里的 
forEach函数来自于ramda库 
最后,真正的 action 文件只需这样 配置 就能实现原有代码的功能
import generator from './generator';
export default generator('USER', [
  {
    name: 'list',
    isAjax: true,
    path: 'apis/user',
    method: 'GET'
  },
  {
    name: 'listSelect'
  }
];
新的问题
如果 action 的 path 里需要通过部分参数来组装时,就需要再优化 generator 了
const getPath = (path, data) => {
  const replaceStrings = path.match(/\$\{\w*\}/g);
  if (!replaceStrings) return path;
  let result = path;
  forEach(replaceString =>
    result = result.replace(replaceString,
      data[replaceString.substring(2, replaceString.length - 1)])
  )(replaceStrings);
  return result;
};
先定义这样一个方法,然后再将原有函数的 endpoint 修改一下
endpoint: getPath(action.path, data),
- 这个函数是利用正则,将 
path中符合${param}格式的参数用data里面对应的key的值替换掉,这样就实现了原有的apis/${tenantId}/userES6 模板字符串的写法 - 要保证调用 
action时候的第一个参数,即data,含有这些需要转换的参数 
最终实现这样的 action,只需要这样配置
{
  name: 'list',
  isAjax: true,
  path: 'apis/${tenantId}/user',
  method: 'GET'
}
调用时候的示例
import userActions from '../../actions/user';
userActions.list({
  tenantId: '11223344'
});
总结
- 经过了这样的改造,代码真的优雅了不少,真正实现了用配置的方式来编写业务,很有成就感。对于这样的项目来讲,只要定好规范,新人很快上手写 
action不是梦。 - 平时写的代码多了,就要多注意这些一直在 
copy和paste的代码,看看是否能够抽取出公共的部分来让代码更加优雅,这才是编程的艺术。 - 写一个 
generator没那么容易,需要考虑各种情况,还要结合实际权衡,让其能更加通用,具备较强可复用性。另外,反复地斟酌和优化代码是必不可少的过程。