优雅地减少redux请求样板代码

在日常开发过程中我们采用react+redux方案进行开发,往往会遇到redux样板代码过多的问题,在不断的抽离过程中,顺手封装了一个redux-middleware。在此进行详细的问题和解决思路。最终代码和示例可以再项目中查看并使用,欢迎使用、建议并star~

抛出问题

使用Redux进行开发时,遇到请求,我们往往需要很复杂的过程,并且这个过程是重复的。我们往往会把一个请求拆分成三个阶段,对应到三个Action Type中去,并且配合redux-thunk中间件,将一个异步action进行拆分,分别对应请求的三个阶段。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 请求的三个状态,开始请求,请求成功,请求失败
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

对应我们需要写以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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个请求接口,我们需要把这些近似相同的代码书写无数遍,显然是很麻烦又不太好的做法,那么我们如何在保证代码流程和可读性的同时,来减少样板代码呢

初步解决方案,使用函数把它封装起来

其实针对这种重复代码,我们第一个想到的就是把它封装成一个函数,将可变因素作为一个参数即可。
但是这个可能稍微复杂一点,因为针对这个函数,我们可能会进行几个不太相关的步骤,或者不能说是步骤,应该说是拿到不懂的我们想要的内容:

  1. 获取三个状态的action
  2. 在请求过程中,分别对三个action进行处理,并且可灵活配置请求参数,请求结果,错误处理等
  3. 自定义initialState,并且在reducer自动对应三个action状态,更新state

由于这个不是我们最终的方案,我直接将代码放出来,阐明我们基本的思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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
})
};
};

通过这个工具函数,我们可以极大的简化整个流程,针对一个请求,我们可以通过以下方式进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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, 并且可以添加额外的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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长什么样子:

1
2
3
4
5
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为这样:

1
2
3
4
5
6
7
{
url: '/api/getData',
params,
types: [ START_ACTION, SUCCESS_ACTION, FAILED_ACTION ],
handleResult,
handleError,
}

在middleware中,我们可以进行以下处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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。保证流程和结构不变的情况下,简化代码。

最终版本

  1. apply middleware
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)
  1. actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 }))
  1. reducer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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~