React-Redux進階(像VUEX一樣使用Redux)

菜的黑人牙膏發表於2019-03-02

前言

Redux是一個非常實用的狀態管理庫,對於大多數使用React庫的開發者來說,Redux都是會接觸到的。在使用Redux享受其帶來的便利的同時, 我們也深受其問題的困擾。

redux的問題

之前在另外一篇文章Redux基礎中,就有提到以下這些問題

  • 純淨。Redux只支援同步,讓狀態可預測,方便測試。 但不處理非同步、副作用的情況,而把這個丟給了其他中介軟體,諸如redux-thunk
    edux-promise
    edux-saga等等,選擇多也容易造成混亂~
  • 囉嗦。那麼寫過Redux的人,都知道action
    educer以及你的業務程式碼非常囉嗦,模板程式碼非常多。但是~,這也是為了讓資料的流動清晰明瞭。
  • 效能。粗暴地、級聯式重新整理檢視(使用react-redux優化)。
  • 分型。原生 Redux-react 沒有分形結構,中心化 store

裡面除了效能這一塊可以利用react-redux進行優化,其他的都是開發者不得不面對的問題,對於程式碼有潔癖的人,囉嗦這一點確實是無法忍受的。

方案目標

如果你使用過VUEX的話, 那麼對於它的API肯定會相對喜歡很多,當然,vuex不是immutable,所以對於時間旅行這種業務不太友好。不過,我們可以自己實現一個具有vuex的簡潔語法和immutable屬性的redux-x(瞎命名)。

先看一下我們想要的目標是什麼樣的?
首先, 我們再./models裡面定義每個子state樹,裡面帶有namespace、state、reducers、effects等屬性, 如下:

export default {
  // 名稱空間
  namespace: `common`,
  // 初始化state
  state: {
    loading: false,
  },
  // reducers 同步更新 類似於vuex的mutations
  reducers: {
    updateLoadingStatus(state, action) {
      return {
        ...state,
        loading: action.payload
      }
    },
  },
  // reducers 非同步更新 類似於vuex的actions
  efffects: {
    someEffect(action, store) {
      // some effect code
      ...
      ... 
      // 將結果返回
      return result
    }
  }
}

複製程式碼

通過上面的實現,我們基本解決了Redux本身的一些瑕疵

1.在effects中存放的方法用於解決不支援非同步、副作用的問題  

2.通過合併reducer和action, 將模板程式碼大大減少  

3.具有分型結構(namespace),並且中心化處理
複製程式碼

如何實現

暴露的介面redux-x

首先,我們只是在外層封裝了一層API方便使用,那麼說到底,傳給redux的combineReducers還是一個redux物件。另外一個則是要處理副作用的話,那就必須使用到了中介軟體,所以最後我們暴露出來的函式的返回值應該具有上面兩個屬性,如下:

import reduxSimp from `../utils/redux-simp` // 內部實現
import common from `./common` // models檔案下common的狀態管理
import user from `./user` // models檔案下user的狀態管理
import rank from `./rank` // models檔案下rank的狀態管理

const reduxX = reduxSimp({
  common,
  user,
  rank
})
export default reduxX
複製程式碼
const store = createStore(
  combineReducers(reduxX.reducers),  // reducers樹
  {},
  applyMiddleware(reduxX.effectMiddler)  //  處理副作用中介軟體
)
複製程式碼

第一步, 我們先實現一個暴露出來的函式reduxSimp,通過他對model裡面各個屬性進行加工,大概的程式碼如下:

const reductionReducer = function() { // somecode }
const reductionEffects = function() { // somecode }
const effectMiddler = function() { // somecode }
/**
 * @param {Object} models
 */
const simplifyRedux = (models) => {
  // 初始化一個reducers 最後傳給combinReducer的值 也是最終還原的redux
  const reducers = {}
  // 遍歷傳入的model
  const modelArr = Object.keys(models)
  modelArr.forEach((key) => {
    const model = models[key]
    // 還原effect
    reductionEffects(model)
    // 還原reducer,同時通過namespace屬性處理名稱空間
    const reducer = reductionReducer(model)
    reducers[model.namespace] = reducer
  })
  // 返回一個reducers和一個專門處理副作用的中介軟體
  return {
    reducers,
    effectMiddler
  }
}
複製程式碼

還原effects

對於effects, 使用的時候如下(沒什麼區別):

props.dispatch({
  type: `rank/fundRankingList_fetch`,
  payload: {
    fundType: props.fundType,
    returnType: props.returnType,
    pageNo: fund.pageNo,
    pageSize: 20
  }
})
複製程式碼

還原effects的思路大概就是先將每一個model下的effect收集起來,同時加上名稱空間作為字首,將副作用的key即type 和相對應的方法value分開存放在兩個陣列裡面,然後定義一箇中介軟體,每當有一個dispatch的時候,檢查key陣列中是否有符合的key,如果有,則呼叫對應的value陣列裡面的方法。

// 常量 分別存放副作用的key即type 和相對應的方法
const effectsKey = []
const effectsMethodArr = []  
/**
 * 還原effects的函式
 * @param {Object} model
 */
const reductionEffects = (model) => {
  const {
    namespace,
    effects
  } = model
  const effectsArr = Object.keys(effects || {})

  effectsArr.forEach((effect) => {
    // 存放對應effect的type和方法
    effectsKey.push(namespace + `/` + effect)
    effectsMethodArr.push(model.effects[effect])
  })
}

/**
 * 處理effect的中介軟體 具體參考redux中介軟體
 * @param {Object} store
 */
const effectMiddler = store => next => (action) => {
  next(action)
  // 如果存在對應的effect, 呼叫其方法
  const index = effectsKey.indexOf(action.type)
  if (index > -1) {
    return effectsMethodArr[index](action, store)
  }
  return action
}
複製程式碼

還原reducers

reducers的應用也是和原來沒有區別:

props.dispatch({ type: `common/updateLoadingStatus`, payload: true })
複製程式碼

程式碼實現的思路就是最後返回一個函式,也就是我們通常寫的redux函式,函式內部遍歷對應名稱空間的reducer,找到匹配的reducer執行後返回結果

/**
 * 還原reducer的函式
 * @param {Object} model 傳入的model物件
 */
const reductionReducer = (model) => {
  const {
    namespace,
    reducers
  } = model

  const initState = model.state
  const reducerArr = Object.keys(reducers || {})

  // 該函式即redux函式
  return (state = initState, action) => {
    let result = state
    reducerArr.forEach((reducer) => {
      // 返回匹配的action
      if (action.type === `${namespace}/${reducer}`) {
        result = model.reducers[reducer](state, action)
      }
    })
    return result
  }
}
複製程式碼

最終程式碼

最終的程式碼如下,加上了一些錯誤判斷:

// 常量 分別存放副作用的key即type 和相對應的方法
const effectsKey = []
const effectsMethodArr = []

/**
 * 還原reducer的函式
 * @param {Object} model 傳入的model物件
 */
const reductionReducer = (model) => {
  if (typeof model !== `object`) {
    throw Error(`Model must be object!`)
  }

  const {
    namespace,
    reducers
  } = model

  if (!namespace || typeof namespace !== `string`) {
    throw Error(`The namespace must be a defined and non-empty string! It is ${namespace}`)
  }

  const initState = model.state
  const reducerArr = Object.keys(reducers || {})

  reducerArr.forEach((reducer) => {
    if (typeof model.reducers[reducer] !== `function`) {
      throw Error(`The reducer must be a function! In ${namespace}`)
    }
  })

  // 該函式即redux函式
  return (state = initState, action) => {
    let result = state
    reducerArr.forEach((reducer) => {
      // 返回匹配的action
      if (action.type === `${namespace}/${reducer}`) {
        result = model.reducers[reducer](state, action)
      }
    })
    return result
  }
}

/**
 * 還原effects的函式
 * @param {Object} model
 */
const reductionEffects = (model) => {
  const {
    namespace,
    effects
  } = model
  const effectsArr = Object.keys(effects || {})

  effectsArr.forEach((effect) => {
    if (typeof model.effects[effect] !== `function`) {
      throw Error(`The effect must be a function! In ${namespace}`)
    }
  })
  effectsArr.forEach((effect) => {
    // 存放對應effect的type和方法
    effectsKey.push(namespace + `/` + effect)
    effectsMethodArr.push(model.effects[effect])
  })
}

/**
 * 處理effect的中介軟體 具體參考redux中介軟體
 * @param {Object} store
 */
const effectMiddler = store => next => (action) => {
  next(action)
  // 如果存在對應的effect, 呼叫其方法
  const index = effectsKey.indexOf(action.type)
  if (index > -1) {
    return effectsMethodArr[index](action, store)
  }
  return action
}

/**
 * @param {Object} models
 */
const simplifyRedux = (models) => {
  if (typeof models !== `object`) {
    throw Error(`Models must be object!`)
  }
  // 初始化一個reducers 最後傳給combinReducer的值 也是最終還原的redux
  const reducers = {}
  // 遍歷傳入的model
  const modelArr = Object.keys(models)
  modelArr.forEach((key) => {
    const model = models[key]
    // 還原effect
    reductionEffects(model)
    // 還原reducer,同時通過namespace屬性處理名稱空間
    const reducer = reductionReducer(model)
    reducers[model.namespace] = reducer
  })
  // 返回一個reducers和一個專門處理副作用的中介軟體
  return {
    reducers,
    effectMiddler
  }
}

export default simplifyRedux
複製程式碼

思考

如何結合Immutable.js使用?

相關文章