在日常開發過程中我們採用react+redux方案進行開發,往往會遇到redux樣板程式碼過多的問題,在不斷的抽離過程中,順手封裝了一個redux-middleware。在此進行詳細的問題和解決思路。最終程式碼和示例可以再專案中檢視並使用,歡迎使用、建議並star~
丟擲問題
使用Redux進行開發時,遇到請求,我們往往需要很複雜的過程,並且這個過程是重複的。我們往往會把一個請求拆分成三個階段,對應到三個Action Type中去,並且配合redux-thunk中介軟體,將一個非同步action進行拆分,分別對應請求的三個階段。如下所示:
// 請求的三個狀態,開始請求,請求成功,請求失敗
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所對應的狀態更改,來相應的對整個請求進行展示。例如:
- 開始請求時進行loading, 需要loading欄位
- 請求成功時結束loading, 修改data
- 請求失敗時結束loading, 展示error
對應我們需要寫以下內容:
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個請求介面,我們需要把這些近似相同的程式碼書寫無數遍,顯然是很麻煩又不太好的做法,那麼我們***如何在保證程式碼流程和可讀性的同時,來減少樣板程式碼呢***
初步解決方案,使用函式把它封裝起來
其實針對這種重複程式碼,我們第一個想到的就是把它封裝成一個函式,將可變因素作為一個引數即可。
但是這個可能稍微複雜一點,因為針對這個函式,我們可能會進行幾個不太相關的步驟,或者不能說是步驟,應該說是拿到不懂的我們想要的內容:
- 獲取三個狀態的action
- 在請求過程中,分別對三個action進行處理,並且可靈活配置請求引數,請求結果,錯誤處理等
- 自定義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。保證流程和結構不變的情況下,簡化程式碼。
最終版本
- 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)
複製程式碼
- 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 }))
複製程式碼
- 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~