优雅地减小redux请求样板代码

在平常开发过程当中咱们采用react+redux方案进行开发,每每会遇到redux样板代码过多的问题,在不断的抽离过程当中,顺手封装了一个redux-middleware。在此进行详细的问题和解决思路。最终代码和示例能够再项目中查看并使用,欢迎使用、建议并star~react

原文连接git

抛出问题

使用Redux进行开发时,遇到请求,咱们每每须要很复杂的过程,而且这个过程是重复的。咱们每每会把一个请求拆分红三个阶段,对应到三个Action Type中去,而且配合redux-thunk中间件,将一个异步action进行拆分,分别对应请求的三个阶段。以下所示:github

// 请求的三个状态,开始请求,请求成功,请求失败
export const START_FETCH = 'START_FETCH'
export const FETCH_SUCCESS = 'FETCH_SUCCESS'
export const FETCH_FAILED = 'FETCH_FAILED'

const startFetch = () => ({
  type: START_FETCH
})
const fetchSuccess = payload => ({
  type: FETCH_SUCCESS,
  payload
})
const fetchFailed = error => ({
  type: FETCH_FAILED,
  error
})

// 在请求的三个阶段中,dispatch不一样的action
export const fetchData = (params) => (dispatch) => {
  // 开始请求
  dispatch(startFetch())

  return fetch(`/api/getData`)
    .then(res => res.json())
    .then(json => {
      dispatch(fetchSuccess(json))
    })
    .catch(error => {
      dispatch(fetchFailed(error))
    })
}
复制代码

同时,咱们须要在reducer中,添加三个action所对应的状态更改,来相应的对整个请求进行展现。例如:json

  • 开始请求时进行loading, 须要loading字段
  • 请求成功时结束loading, 修改data
  • 请求失败时结束loading, 展现error

对应咱们须要写如下内容:redux

const initialData = {
  data: {},
  loading: false,
  error: null
}

const data = (state = initialData, action) => {
  switch(action.type) {
    case START_FETCH:
      return {
        ...state,
        loading: true,
        error: null
      }
    case FETCH_SUCCESS:
      return {
        ...state,
        loading: false,
        data: action.payload
      }
    case FETCH_FAILED:
      return {
        ...state,
        loading: false,
        error: action.error
      }
    default:
      return state
  }
})
复制代码

针对一个完整健壮的请求,咱们每每须要把上述的代码所有写一遍。假设咱们一个页面有N个请求接口,咱们须要把这些近似相同的代码书写无数遍,显然是很麻烦又不太好的作法,那么咱们***如何在保证代码流程和可读性的同时,来减小样板代码呢***api

初步解决方案,使用函数把它封装起来

其实针对这种重复代码,咱们第一个想到的就是把它封装成一个函数,将可变因素做为一个参数便可。 可是这个可能稍微复杂一点,由于针对这个函数,咱们可能会进行几个不太相关的步骤,或者不能说是步骤,应该说是拿到不懂的咱们想要的内容:app

  1. 获取三个状态的action
  2. 在请求过程当中,分别对三个action进行处理,而且可灵活配置请求参数,请求结果,错误处理等
  3. 自定义initialState,而且在reducer自动对应三个action状态,更新state

因为这个不是咱们最终的方案,我直接将代码放出来,阐明咱们基本的思路:异步

import update from "immutability-helper";

// 根据actions来返回目的reducer, 此reducer会自动对单个过程更新state
// 而且能够增长自定义的修改
const reducerCreator = actions => (initState, otherActions) => {
  const resultInitState = Object.assign({}, initState, {
    isFetching: true,
    isError: false,
    ErrMsg: ""
  });
  const { START_ACTION, SUCCESS_ACTION, FAILED_ACTION } = actions;

  return (state = resultInitState, action) => {
    let ret;
    switch (action.type) {
      case START_ACTION:
        ret = update(state, {
          isFetching: {
            $set: true
          },
          isError: {
            $set: false
          },
          ErrMsg: {
            $set: ""
          }
        });
        break;
      case SUCCESS_ACTION:
        ret = update(state, {
          isFetching: {
            $set: false
          }
        });
        break;
      case FAILED_ACTION:
        ret = update(state, {
          isFetching: {
            $set: false
          },
          isError: {
            $set: true
          }
        });
        break;
      default:
        ret = state;
    }

    return otherActions(ret, action);
  };
};

// 1.建立三个action
// 2.执行请求函数, 在请求中咱们能够任意的格式化参数等
// 3.请求过程当中执行三个action
// 4.根据三个action返回咱们的reducer
export default (action, fn, handleResponse, handleError) => {
  const START_ACTION = Symbol(`${action}_START`);
  const SUCCESS_ACTION = Symbol(`${action}_SUCCESS`);
  const FAILED_ACTION = Symbol(`${action}_FAILED`);

  const start = payload => ({
    type: START_ACTION,
    payload
  });
  const success = payload => ({
    type: SUCCESS_ACTION,
    payload
  });
  const failed = payload => ({
    type: FAILED_ACTION,
    payload
  });
  return {
    actions: {
      [`${action}_START`]: START_ACTION,
      [`${action}_SUCCESS`]: SUCCESS_ACTION,
      [`${action}_FAILED`]: FAILED_ACTION
    },
    method: (...args) => (dispatch, getState) => {
      dispatch(start());
      return fn(...args, getState)
        .then(r => r.json())
        .then(json => {
          if (json.response_code === 0) {
            const ret = handleResponse
              ? handleResponse(json, dispatch, getState)
              : json;
            dispatch(success(ret));
          } else {
            dispatch(failed(json));
          }
        })
        .catch(err => {
          const ret = handleError ? handleError(err) : err;
          dispatch(failed(err));
        });
    },
    reducerCreator: reducerCreator({
      START_ACTION,
      SUCCESS_ACTION,
      FAILED_ACTION
    })
  };
};
复制代码

经过这个工具函数,咱们能够极大的简化整个流程,针对一个请求,咱们能够经过如下方式进行:函数

const getDataFn = params => {
  return fetch("/api/getData", {
    method: "POST",
    headers: {
      "Content-type": "application/json; charset=UTF-8"
    },
    body: JSON.stringify(params)
  });
};

export const {
  // 三个action
  actions: getDataActions,
  // 建立reducer
  reducerCreator: getDataReducerCreator,
  // 请求,触发全部的过程
  method: getData
} = reduxCreator("GET_DATA", getDataFn, res => res.data);
复制代码

在reducer中,咱们能够直接使用reducerCreator建立reducer, 而且能够添加额外的内容工具

const initialData = {
  list: []
}

// 最终的reducer,包含请求和错误状态,且根据请求自动更新
const threatList = threatListReducerCreator(initialData, (state, action) => {
  switch (action.type) {
    case getDataActions.GET_DATA_SUCCESS:
      return update(state, {
        list: {
          $set: action.payload.items
        }
      });
    default:
      return state;
  }
})
复制代码

经过这种方式,咱们极大的减小了整个过程的代码,而且能够在每一个过程当中灵活的加入咱们想要的东西。 配合我封装的react组件中的Box组件,很方便的实现 请求->loading->展示内容的过程。

可是,老是隐约以为这个代码有些不舒服,不舒服在哪儿呢? 没错,虽然它很大程度的简化了代码,可是使用这个工具函数后,极大的***改变了整个redux代码的结构***, 整个函数使用过程及***语义化十分不明显,咱们很难一眼看出来咱们都作了什么***。 而且,不熟悉Api的人用起来会十分难受

所以,咱们对以上代码进行改善,以达到咱们最终的要求:优雅

引子

Redux借鉴Koa的中间件机制,也给咱们提供了一个很好的middleware使用。具体的原理咱们在此不进行赘述,咱们来看下一个基础的middleware长什么样子:

const logMiddleware = store => next => action => {
  console.log(action)
  next(action)
  console.log(action, 'finish')
}
复制代码

咱们会看到,在一个middleware中,咱们能够拿到store和action, 而且自动的执行下一个中间件或者action。 基本获取了咱们全部须要的内容,咱们能够直接在将请求过程当中的固定代码,交给middleware来作!

使用redux-middleware简化流程

咱们能够将分发action的过程在此自动进行,相信不少人都会这么作,咱们只须要定义咱们的特殊action的格式,而且针对此action进行特殊处理便可。好比咱们定义咱们的请求action为这样:

{
  url: '/api/getData',
  params,
  types: [ START_ACTION, SUCCESS_ACTION, FAILED_ACTION ],
  handleResult,
  handleError,
}
复制代码

在middleware中,咱们能够进行如下处理:

const fetchMiddleware = store => next => action => {
  // 普通action直接执行
  if (!action.url || !Array.isArray(action.types)) {
    return next(action)
  }
  // 处理咱们的request action
  const {
    handleResult = val => val,
    handleError = error => error,
    types, url, params
  } = action
  const [ START, SUCCESS, FAILED ] = types

  next({
    type: START,
    loading: true,
    ...action
  })
  return fetchMethod(url, params)
    .then(handleResponse)
    .then(ret => {
      next({
        type: SUCCESS,
        loading: false,
        payload: handleResult(ret)
      })
      return handleResult(ret)
    })
    .catch(error => {
      next({
        type: FAILED,
        loading: false,
        error: handleError(error)
      })
    })
}
复制代码

同时,咱们提供actionCreator, reducerCreator来建立对应的action, 和reducer。保证流程和结构不变的状况下,简化代码。

最终版本

  1. apply middleware
import createFetchMiddleware from 'redux-data-fetch-middleware'
import { applyMiddleware } from 'redux'
import thunk from 'redux-thunk'

// 设置公用的请求函数
const fetchMethods = (url, params) => fetch(url, {
    method: "post",
    headers: {
      "Content-type": "application/json; charset=UTF-8"
    },
    body: JSON.stringify(params)
  })

// 设置共用的处理函数,如进行统一的错误处理等
const handleResponse = res => res.json()

const reduxFetch = createFetchMiddleware(fetchMethods, handleResponse)

const middlewares = [thunk, reduxFetch]

applyMiddleware(...middlewares)
复制代码
  1. actions
import { actionCreator } from 'redux-data-fetch-middleware'

// 建立三个action
export const actionTypes = actionCreator('GET_USER_LIST')

export const getUserList = params => ({
  url: '/api/userList',
  params: params,
  types: actionTypes,
  // handle result
  handleResult: res => res.data.list,
  // handle error
  handleError: ...
})

// 能够直接dispatch,自动执行整个过程
dispatch(getUserList({ page: 1 }))
复制代码
  1. reducer
import { combineReducers } from 'redux'
import { reducerCreator } from 'redux-data-fetch-middleware'
import { actionTypes } from './action'

const [ GET, GET_SUCCESS, GET_FAILED ] = actionTypes

// userList会自动变成 {
// list: [],
// loading: false,
// error: null
// }
// 而且当GET, GET_SUCCESS and GET_FAILED改变时,会自动改变loading,error的值
const fetchedUserList = reducerCreator(actionTypes)

const initialUserList = {
  list: []
}

const userList = (state = initialUserList, action => {
  switch(action.type) {
    case GET_SUCCESS:
      return {
        ...state,
        action.payload
      }
  }
})

export default combineReducers({
  userList: fetchedUserList(userList)
})
复制代码

总结

从开始的问题抛出到解决思路到不断完善的过程,是解决问题的标准流程。经过此次封装,咱们很好的解决了平常开发过程当中Redux请求代码冗余的问题,而且也充分的了解了redux-middleware的机制。欢迎指正且star~

相关文章
相关标签/搜索