簡化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還是挺麻煩的。