重新思考 redux

redux 基础模式

基础 redux 的完整模式为:

dispatch action -> redux middlewares -> reducers -> global state -> components

该模式存在如下问题:

  • 多文件:一个 dispatch 动作从发起到被 reducer 接收,期间通过某个具体的 action type 进行指令的传递。在传统的 redux 架构中,action typeaction creatorreducer 可能会散落在多个文件中,对于大型项目,这种 redux 架构会给开发者带来额外的开发和维护成本;在 Ducks 架构中,action typeaction creatorreducer 虽然被分配在同一个文件中,但是三者仍然是割裂的,需要通过开发者定义的 action type 来串联,这些 action type 对于 react 来说又是无感知的,建议移除 action type 的概念,同时将 action creatorreducer 结合起来使用。

  • dispatch 绑定:在使用 connect 的过程中,往往需要开发者通过 mapDispatchToProps 手动绑定 dispatch 函数和从别处引入的 action creator,这种操作十分繁琐,更好地方式是在 mapDispatchToProps 中允许开发者按需加载已经绑定 dispatch 函数的 action creator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// bad action creator
import {increment} from 'actions/count'
export default connect(
state => state.count,
dispatch => ({
increment: params => dispatch(increment(params))
})
)
// good action creator
export default connect(
state => state.count,
dispatch => ({
increment: dispatch.count.increment
})
)
  • reducer 描述形式redux 官方规定 reducer 必须是一个纯函数,reducer 可根据接收到的不同 action type 来计算新的 state。比起在纯函数中使用复杂的 switch-caseif-else 语句, 用对象的形式描述 reducer,从代码层面看显然更加语义化,编写也更加简单。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const INCREMENT = 'INCREMENT'
const DECREMENT = 'DECREMENT'
// pure function reducer
const reducer = (state, action) => {
switch(action.type){
case INCREMENT:
return state + action.payload
case DECREMENT:
return state - action.payload
default:
return 0
}
}
// object reducer
const reducer = {
state: 0,
INCREMENT: (state, action) => state + action.payload,
DECREMENT: (state, action) => state - action.payload
}
  • payload 字段规范reducer 接收的 action 参数中包含有开发者期望传入的参数,redux 官方建议遵守 FSA 规范,但在实际使用时,redux 并未强制使用该规范,但对一个项目而言,统一字段有利于项目的维护,而 action type 比较鸡肋,可以不予考虑,errormeta 字段可以统一称为 payload,因此上述例子中 reducer 接收的参数最好强制为是 payload
1
2
3
4
5
const reducer = {
state: 0,
INCREMENT: (state, payload) => state + payload,
DECREMENT: (state, payload) => state - payload
}
  • 异步处理dispatch action 可以划分为两类,同步 action 和异步 action,异步 action 本质也是在不同时机发出同步 action
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const reducer = {
state: {loading: false, list: []},
reducers: {
setLoading: (state, payload) => ({...state, loading: payload}),
setList: (state, payload) => ({...state, list: payload})
},
effects: {
create: async (state, payload) => {
this.setLoading(true)
const list = await axios.get('/list/data', {params: payload})
this.setList(list)
this.setLoading(false)
}
}
}
  • 领域建模redux 本身是一套状态管理机制,并不支持与后端进行交互,因此需要用户自己去定义请求方式将请求与 redux 结合起来使用。由于前端缺少类似后端的 service 层和领域层,对于某个请求,常规的做法是定义一个请求函数,然后在 redux 或者 react 中去引用并调用该函数,这么做很多情况下比较繁琐,语义化差且不利于对后端的业务逻辑进行建模,这里提供一种 redux 结合领域的方式:通过一份配置 meta 文件去描述后端的领域模型 DM,然后将 DM 注入到 effects
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
// meta.js
export default {
protocol: location.protocol, // 协议
host: location.host, // 主机
resources:{
list: {
select: 'GET::/api/list/query',
delete: 'DELETE::/api/list/delete/:id'
}
}
}
// reducer.js
const reducer = {
state: {loading: false, list: []},
reducers: {
setLoading: (state, payload) => ({...state, loading: payload}),
setList: (state, payload) => ({...state, list: payload})
},
effects: (dispatch, DM) => ({
create: async (state, payload) => {
const {setLoading, setList} = dispatch.list
setLoading(true)
// 调用 DM 的 list 模型的 query 方法
const list = await DM.select('list', payload)
setList(list)
setLoading(false)
}
})
}
  • immutable侵入:前端项目普遍使用了 immutable 库,immutable 对项目侵入程度比较大,使用方式又与原生的 js 类型操作有很大异同,加上繁琐的 fromJS 和 toJS 操作,会在一定程度上提升开发成本,可以考虑用 immer 替换并集成到 redux 的底层使用。