我是如何一步步「改造」redux的

從Vue換到React+Redux進行開發已經有半年多的時間,總的來講體驗是很好的,對於各類邏輯和業務組件的抽象實在是方便的不行,高階組件,洋蔥模型等等給我帶來了不少編程思想上的提高。可是在使用Redux開發的過程當中仍是感受不太順手,本文將闡述我是如何對Redux進行一步步「改造」以適應我的和團隊開發需求的。
過程當中的示例和結果放在了easy-redux,歡迎star。
原文連接前端

問題

在使用Redux開發的過程當中逐漸發現,雖然咱們已經將UI組件和業務組件儘量的進行抽離,儘量的保證reduceractions的複用性,
可是咱們仍是會花費大量的時間來書寫近乎相同的代碼。尤爲是咱們組內但願秉承一個原則:儘可能將全部的操做及狀態修改都交由action來執行,方便咱們對問題進行定位。當我在某大型前端交流羣裏還看到「不用Redux,不想加班」的說法時,不得不感嘆,須要作些努力來解決我目前的問題了。react

是的,Redux對我來講,太複雜了git

針對一個簡單的操做,咱們須要進行如下步驟來完成:github

1.定義action編程

export const CHANGE_CONDITION = 'CHANGE_CONDITION'

2.定義一個對應的action建立函數redux

export const changeCondition = condition => ({
  type: CHANGE_CONDITION,
  condition
})

3.引入action, 定義reducer, 在複雜的switch語句中,對對象進行更改api

import { CHANGE_CONDITION } from '@actions'

const condition = (state = initCondition, action) => {
  switch(action.type) {
    case CHANGE_CONDITION:
      return ...
    default:
      return state
  }
}

4.在須要時,引入action建立函數, 並將對應的state進行鏈接app

import { changeCondition } from 'actions'
@connect(...)

我只是想作一個簡單的狀態修改呀!異步

可能咱們會說,這樣拆分可以保證咱們整個項目的規範化,加強業務的可預測性與錯誤定位能力。
可是隨着項目的不斷擴大,每一個頁面都有一堆action須要我加的時候,實在是讓人頭痛啊。函數

並且,針對請求的修改,咱們每每要把action拆分紅START,SUCCESS,FAILED三種狀態,reducer裏須要進行三次修改。並且每每
針對這些修改,咱們進行的處理都是大體相同的:更新loading狀態,更新數據,更新錯誤等等。

因此說,咱們如何在保證redux的設計原則以及項目規範性上,對其進行「簡化改造」,是我這裏須要解決的問題。

使用middleware簡化請求

針對請求的處理,我以前也寫過一篇文章優雅地減小redux請求樣板代碼, 經過封裝了一個redux中間件react-fetch-middleware
來對請求代碼進行優化。

大體思路以下:

1.action建立函數返回的內容爲一個包含請求信息的對象,幷包含須要分發的三個action,這三個action能夠經過actionCreator進行建立

import { actionCreator } from 'redux-data-fetch-middleware'

// create action types
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: ...
})

2.在redux中間件中,針對以上格式的action進行處理,首先進行請求,並分發請求開始的action,
在請求成功和失敗時,分別分發對應的action

const applyFetchMiddleware = (
  fetchMethod = fetch,
  handleResponse = val => val,
  handleErrorTotal = error => error
) =>
  store => next => action => {
    // 判斷action的格式
    if (!action.url || !Array.isArray(action.types)) {
      return next(action)
    }
    // 獲取傳入的三個action
    const [ START, SUCCESS, FAILED ] = action.types

    // 在不一樣狀態分發action, 並傳入loading,error狀態
    next({
      type: START,
      loading: true,
      ...action
    })
    return fetchMethod(url, params)
      .then(ret => {
        next({
          type: SUCCESS,
          loading: false,
          payload: handleResult(ret)
        })
      })
      .catch(error => {
        next({
          type: FAILED,
          loading: false,
          error: handleError(error)
        })
      })
  }

3.將reducer進行對應的默認處理,使用reducerCreator建立的函數中自動進行對應處理,而且提供二次處理的機制

const [ GET, GET_SUCCESS, GET_FAILED ] = actionTypes

// 會在這裏自動處理分發的三個action
const fetchedUserList = reducerCreator(actionTypes)

const userList = (state = {
  list: []
}, action => {
  // 二次處理
  switch(action.type) {
    case GET_SUCCESS:
      return {
        ...state,
        action.payload
      }
  }
})
export default combineReducers({
  userList: fetchedUserList(userList)
})

再進一步,簡化Redux Api

通過前一步對請求的簡化,咱們已經能夠在保證不改變redux原則和書寫習慣的基礎上,極大的簡化請求樣板代碼。
針對普通的數據處理,咱們是否是能夠更進一步?

很高興看到這個庫: Rematch
, 對Redux Api進行了極大的簡化。

可是有些功能和改進並非咱們想要的,所以我僅對我須要的功能和改進點進行說明,並用本身的方式進行實現。咱們來一步步看看
咱們須要解決的問題以及如何解決的。

1.冗長的switch語句

針對reducer,咱們不但願重複的引用定義的各個action, 而且去掉冗長的switch判斷。其實咱們能夠將其進行反轉拆分,將每個action定義爲標準化的reducer, 在其中對state進行處理.

const counter = {
  state: 1,
  reducers: {
    add: (state, payload) => state + payload,
    sub: (state, payload) => state - payload
  }
}

2.複雜的action建立函數

去掉以前的action和action建立函數,直接在actions中進行數據處理,並與對應的reducer進行match

export const addNum = num => dispatch => dispatch('/counter/add', num)

咱們會看到,與reducer進行match時,咱們使用了'/counter/add'這種命名空間的方式,
目的是在保證其直觀性的同時,保證action與其reducer是一一對應的。

咱們能夠經過加強的combinceReducer進行命名空間的設定:

const counter1 = {
  ...
}
const counter2 = {
  ...
}

const counters = combinceReducer({
  counter1,
  counter2
})

const list = {
  ...
}
// 設置大reducer的根命名空間
export default combinceReducer({
  counters,
  list
}, '/test')

// 咱們能夠經過這樣來訪問
dispatch('/test/counters/counter1/add')

3.別忘了請求

針對請求這些異步action,咱們能夠參考咱們以前的修改, dispatch一個對象

export const getList = params => dispatch => {
  return dispatch({
    //對應到咱們想要dispatch的命名空間
    action: '/list/getList',
    url: '/api/getList',
    params,
    handleResponse: res => res.data.list,
    handleError: error => error
  })
}

同時,咱們在reducer中進行簡單的處理便可,依舊能夠進行默認的三個狀態處理

const list = {
  // 定義reducer頭,會自動變爲getList(開始請求),getListSuccess,getListFailed
  // 並進行loading等默認處理
  fetch: 'getList'
  state: {
    list: []
  },
  reducers: {
    // 二次處理
    getListSuccess: (state, payload) => ({
      ...state,
      list: payload
    })
  }
}

與項目進行整合

咱們會看到,咱們已經將redux的api進行了極大的簡化,可是依舊保持了原有的結構。目的有如下幾點:

  1. 依舊遵循默認原則,保證項目的規範性
  2. 經過約定和命名空間來保證action和reducer的match
  3. 底層仍是使用redux實現,這些只不過是語法糖
  4. 保證與老項目的兼容性

原有的數據流變成了這樣:
圖片描述

所以,咱們是在redux的基礎上進行二次封裝的,咱們依然保證了原有的Redux數據流,保證數據的可回溯性,加強業務的可預測性與錯誤定位能力。這樣能極大的保證與老項目的兼容性,因此咱們須要作的,只是對action和reducer的轉化工做

1.combinceReducer返回原格式的reducer

咱們經過新的combinceReducer,將新的格式,轉化爲以前的reducer格式,並保存各個reducer其和對應的action的命名空間。

代碼簡單示意:

//獲取各reducers裏的方法
const actionNames = Object.keys(reducers)
const resultActions = actionNames.map(action => {
  const childNamespace = `${namespace}/${action}`
  // 將action存入namespace
  Namespace.setActionByNamespace(childNamespace)
  return {
    name: Namespace.toAction(childNamespace),
    fn: reducers[action]
  }
})

// 返回默認格式
return (state = inititalState, action) => {
  // 查詢action對應的新的reducer裏的方法
  const actionFn = resultActions.find(cur => cur.name === action.type)
  if (actionFn) {
    return actionFn.fn && actionFn.fn(state, action.payload)
  }
  return state
}

2.新的action建立函數最終dispatch出原格式的action

咱們須要把這樣格式的函數,轉化成這樣

count => dispatch => dispatch('/count/add', count)

//or
params => dispatch => { dispatch('/count/add', 1), dispatch('/count/sub', 2) }

//結果
count => ({ type: 'count_add', payload: count })

這裏的處理比較複雜,其實就是改造咱們的dispatch函數

action => params => (dispatch, getstate) => {
  const retDispatch = (namespace, payload) => {
    return dispatch({
      type: Namespace.get(namespace),
      payload
    })
  }
  return action(params)(retDispatch, getstate)
}

總結

經過對Redux Api的改造,至關於二次封裝,已經很大的簡化了目前在項目中的樣板代碼,而且在項目中很順暢的使用。

針對整個過程,其實還有幾個能夠改進的地方:

  • actions的轉化過程,交由中間件處理
  • 性能問題,目前至關於多作了一層轉化,可是目前影響不大
  • reducer,action複用

有興趣的話,歡迎探討~ 附上github easy-redux

相關文章
相關標籤/搜索