React 实践:编写自己项目的 Redux action generator

Author Avatar
Splendour 8月 17, 2016

背景

使用 ReactRedux 也有一段时间了,从第一次尝试使用之后就开始按一种方式写代码,思路很清晰,但就是有一个问题,感觉每写一个 action 都是在 copypaste,然后修改其中的部分内容,最近想优化代码所以尝试着编写一个 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 的名字,需要是唯一的; pathmethod 分别代表 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'
  }
];

新的问题

如果 actionpath 里需要通过部分参数来组装时,就需要再优化 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}/user ES6 模板字符串的写法
  • 要保证调用 action 时候的第一个参数,即 data,含有这些需要转换的参数

最终实现这样的 action,只需要这样配置

{
  name: 'list',
  isAjax: true,
  path: 'apis/${tenantId}/user',
  method: 'GET'
}

调用时候的示例

import userActions from '../../actions/user';
userActions.list({
  tenantId: '11223344'
});

总结

  • 经过了这样的改造,代码真的优雅了不少,真正实现了用配置的方式来编写业务,很有成就感。对于这样的项目来讲,只要定好规范,新人很快上手写 action 不是梦。
  • 平时写的代码多了,就要多注意这些一直在 copypaste 的代码,看看是否能够抽取出公共的部分来让代码更加优雅,这才是编程的艺术。
  • 写一个 generator 没那么容易,需要考虑各种情况,还要结合实际权衡,让其能更加通用,具备较强可复用性。另外,反复地斟酌和优化代码是必不可少的过程。