優雅地減小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~

相關文章
相關標籤/搜索