1. 程式人生 > >簡化redux中的action和reducer

簡化redux中的action和reducer

如何讓action和reducer更簡單,這就是本文所需要記錄的。可直接跳到改進部分。

前言

最近做的專案中,也使用了redux。redux是基於純函式的,為了保證其純度,它的reducer的要求是S’ = f(S)的這種形式。但是在實際專案中,我們有很多網路請求,那麼要求reducer的形式是S’ = await f(Async)(S)的形態。但是在reducer中這是不允許的(為了保證其純度),所以才會出現了各種各樣的基於redux非同步庫,redux-thunk,redux-promise,redux-promise-middleware,redux-saga……

如何設計action

本文不是探討幾種非同步方案的差別的,這裡講到了幾種方案。專案中我使用的是redux以及非同步庫redux-thunk。我將說說我是如何設計使redux這種非同步使用起來更簡單一點。

樂觀更新

首先,我拋棄了樂觀更新。通常一個action—-reducer的流程是這樣的。

//action types
const GET_DATA = 'GET_DATA';
const GET_DATA_SUCCESS = 'GET_DATA_SUCCESS';
const GET_DATA_FAILED = 'GET_DATA_FAILED';

//action creator
const getDataAction
= (someData) =>
(dispatch, getState) => { dispatch({ type: GET_DATA, data: someData}); fetch() // 我使用的是fetch,並對fetch做了封裝 .then(res => res.json()) // 以json資料為例 .then(jsonData => { dispatch({ type: GET_DATA_SUCCESS }) }) .catch(err => { dispatch({ type: GET_DATA_FAILED }) }) } //
reducer const dataReducer = (state, action) => { switch(action.type) { case GET_DATA : return oldState; case GET_DATA_SUCCESS : return successState; case GET_DATA_FAILED : return errorState; } }

這僅僅是最簡單的reducer了,我還沒有寫任何業務邏輯部分的程式碼。可以看到這種寫法還是很麻煩(三個常量,三個action,三個case)。第一個dispatch({ type: GET_DATA, data: someData});就是為了樂觀更新。假如我們現在有這樣一個業務需求—需要新增一個商品到購物車。

  • 點選新增到購物車按鈕

  • dispatch一個action,action中攜帶著商品相關資料

  • 在action的處理中,需要傳送非同步請求更新資料庫的購物車。這時候,如果我們在發起非同步請求前先做一個這樣的action。dispatch({ type: UPDATE_SHOPCART, data: someProduct});,這時候reducer收到了這個action,就會更新store中關於購物車的部分。然後在非同步請求成功或者失敗後分別還會dispatch一個action。如果成功了,代表我們之前更新store中關於購物車的部分是無誤的。如果失敗了,我們再從store的購物車中去掉關於這個商品的資訊即可。這就是樂觀更新。
    這就和我們發微信類似,在我們點擊發送的那一瞬間,訊息已經進入對話方塊了(樂觀更新,總是假設它是成功的),然後如果由於網路問題傳送失敗了就會額外做一個標誌。

這個專案中,我沒有使用樂觀更新,我覺得太麻煩。而且對使用者來說,看的是新增成功了,然後又來一個新增失敗的通知,想必體驗也不會好。

除了樂觀更新

除了樂觀更新到需要的一個action,還有兩個action。分別表示成功和失敗。一般而言會這樣:

fetch() // 我使用的是fetch,並對fetch做了封裝
  .then(res => res.json()) // 以json資料為例
  .then(jsonData => {
    dispatch({ 
      type: UPDATE_SHOPCART_SUCCESS,
      payload: {
        data: jsonData
      } 
    })
  })
  .catch(err => {
    dispatch({ 
      type: UPDATE_SHOPCART_FAILED,
      payload: { err }
    })
  })

嗯,這種情況還是需要定義兩個action(兩個額外的常量,兩個case)。我覺得還是很麻煩。我於是就想著還可以怎麼改進,讓寫法更簡單?讓我更加少寫點程式碼?

改進① – 兩種情況我只需要一個action

還以更新購物車為例:

// action
const updateShopcart = (someProduct) => (dispatch, getState) {
  fetch(SOME_API, SOME_OPTIONS) 
  .then(res => res.json()) // 假設是json資料
  .then(jsonData => {
    dispatch(jsonData => {
      dispatch({ // 實際情況中該plain object可封裝成一個函式
        type: UPDATE_SHOPCART,
        payload: {
          returnedData: jsonData 
          data: someProduct 
        }
      });
    })
  }) 
  .catch(err => {
    dispatch({
      type: UPDATE_SHOPCART,
      payload: {
        err,
      }
    });
  })
}

我只是將action合二為一了而已,並攜帶不同的欄位。這樣的話在recuder中處理就稍顯複雜。

// reducer
const shopcart = (state, action) => {
  switch(action.type) {
    case 'UPDATE_SHOPCART':
      if(Object.prototype.toString.call(action.payload.err) === '[object Error]') { // 我喜歡這樣去判斷
        return errorState; // 這裡根據錯誤返回實際情況,或給出一個錯誤欄位等
      } 
      if(action.payload.returnedData.code === 1) { // 這是因為後端返回的code為1才表示成功
        return {
          ...state,
          shopcart: [action.payload.data, ...shopcart] // 更新購物車,就不需要再去伺服器請求資料了
        }
      }
  }
}

可以看到,這只是reducer對一個數據的處理,對於每個非同步請求都要這樣處理,還是挺麻煩的(雖然已經簡化了action)。經過一番思考,再次改進。

改進② – 使用中介軟體來處理錯誤

redux中,中介軟體在action和reducer之間。也就是說,所有的action都要經過中介軟體。如果在中介軟體中處理了這些錯誤,豈不是很美妙?關於中介軟體,我在[reudx原始碼分析中有提到]。(http://blog.csdn.net/real_bird/article/details/72872566)

const errorReporter = store => next => action => {
  if(Object.prototype.toString.call(action.payload.err) === '[object Error]') {
    switch(action.type) {
      // 根據不同的action處理不同的錯誤,為了不讓業務處理邏輯程式碼注入到中介軟體,可根據不同的action.type返回不同的action
    }
    // 如果要統一處理錯誤,就不需要上面的switch了,可直接返回一個特別的action,比如
    return next({
      type: '@FAIL'
      payload: { 
        err: action.payload.err, 
        message: action.payload.returnedData.msg // // 後臺返回的錯誤訊息
      } 
    })
  } else { // 後臺返回了資料的情況
    let statusCode = action.payload.returnedData.code;
    if(statusCode === 1) { // 協定為1表示成功
      return next(action); // 這樣在reducer中只處理成功的情況
    } else {
      // ......
    }
  }
}

這樣reducer就簡單了。

// reducer
const shopcart = (state, action) => {
  switch(action.type) {
    case 'UPDATE_SHOPCART':   
      return {
        ...state,
        shopcart: [action.payload.data, ...shopcart] // 更新購物車,就不需要再去伺服器請求資料了
      }
    case '@FAIL': 
      return {
        ...state,
        err: action.payload.err,
        message: action.payload.message,
      }
  }
}

這就是在這個專案中我想到了兩點改進reducer的方法。不過為了再減少麻煩,我直接在中介軟體中處理錯誤了,比如給出通知訊息什麼的。讓中介軟體顯得不乾淨,但是少了很多程式碼。這不正是“美觀”與“簡單”之間的一次博弈嗎。很多時候都是,無法兼顧。redux讓資料流更清晰,但也讓我們多寫了很多程式碼。

最後

我覺得redux還是挺麻煩的。