本文是 《使用 RxJS + Redux 管理应用状态》系列第三篇文章,将介绍咱们在使用 Redux 时的困惑,如何从新思考 Redux 定下的范式,以及咱们能为此作出的努力。返回第一篇:使用 redux-observable 实现组件自治html
本系列的文章地址汇总:前端
首先要明确的是,Redux 并非 React 独有的一个插件,它是顺应前端组件化开发潮流而诞生的一种状态管理模型,你在 Vue 或者 Angular 中也可使用这个模型。react
目前,你们都比较承认的是,某一时刻的应用或者组件状态,将对应此时应用或者组件的 UI:git
UI = f(state)
复制代码
那么,在前端组件化开发的时候,就须要思考两个问题:github
组件所具备的状态,一搬来源于两个方面:编程
状态源为组件输送了其须要的状态,进而,组件的外观形态也获得了确认。在简单工程和简单组件中,咱们思考了状态来源也就好了,若是引入额外的状态管理方案(例如咱们为一个使用 Redux 管理一个按钮组件的状态),反而会加剧每一个组件的负担,形成了多余的抽象和依赖。redux
而对于大型前端工程和复杂组件来讲,其每每具备以下特色:后端
在这种场景下,朴素的状态管理就显得捉襟见肘了,主要体如今下面几个方面:api
Redux 正是要去解决这些问题,从而让大型前端工程的状态更加可控。Redux 提出了一套约定模型,让状态的更新和派发都集中了:bash
Redux 所使用的模型是受到了 Elm 的启发:
在 Elm 中,流动于应用中的是消息(msg) :一个由**消息类型(type)所标识,而且携带了内容(payload)**的数据结构。消息决定了数据模型(model)怎么更新,而数据又决定了 UI 形态。
而在 Redux 中,消息被称替代为动做(action),而且使用 reducer 来描述状态随行为的变迁。另外,与 Elm 不一样的是,Redux 专一于状态管理,而再也不处理视图(View),所以 ,Redux 也不是分型的(关于分型架构的介绍,能够看 的博文)。
在了解到 Redux 的利好,或者被 Redux 的流行所吸引后,咱们引入 Redux 做为应用的状态管理器,这让整个应用的状态变更都变得无比清晰,状态在一条链路上涌动,咱们甚至能够回到或者前进到某个状态。然而,Redux 就真的天衣无缝吗?
Redux 固然不完美,它最困扰咱们的就是下面两个方面:
假定前端须要从服务端拉取一些数据并进行展现,在 Redux 的模式下,完成从数据拉取到状态更新,就须要经历:
(1)定义若干的 action type:
const FETCH_START = 'FETCH_START'
const FETCH_SUCCESS = 'FETCH_SUCCESSE'
const FETCH_ERROR = 'FETCH_ERROR'
复制代码
(2)定义若干 action creator,这里假定咱们使用 redux-thunk 驱动异步任务:
const fetchSuccess = data => ({
type: FETCH_START,
payload: { data }
})
const fetchError = error => ({
type: FETCH_ERROR,
payload: { error }
})
const fetchData = (params) => {
return (dispatch, getState) => {
return api.fetch(params)
.then(fetchSuccess)
.catch(fetchError)
}
}
复制代码
(3)在 reducer 中,对不一样 action type,经过 switch-case 声明不一样的状态更新方式:
function reducer(state = initialState, action) {
const { type, payload } = action
switch(action.type){
case FETCH_START: {
return { ...state, loading: true }
}
case FETCH_SUCCESS: {
return { ...state, loading: false, data: payload.data }
}
case FETCH_ERROR: {
return { ...state, loading: false, data: null, error: payload.error}
}
}
}
复制代码
这个流程带来的问题是:
当咱们受困于 Redux 的负面影响时,切到其余的状态管理方案(例如 mobx 或者 mobx-state-stree),也不太现实,一方面是迁移成本大,一方面你也不知道新的状态管理方案是否就是银弹。可是,对 Redux 的负面影响无动于衷或者忍气吞声,也只会让问题越滚越大,直到失控。
在开始讨论如何更好地 Redux 以前,咱们须要明确一点,样板代码和异步能力的缺少,是 Redux 自身设计的结果,而非目的,换句话说,Redux 设计出来,并非要让开发者去撰写样本代码,或者去纠结怎么处理异步状态更新。
咱们须要再定义一个角色,让他来代替咱们去写样板代码,让他给予咱们最优秀的异步任务处理能力,让他负责一切 Redux 中恶心的事儿。所以,这个角色就是一个让 Redux 变得更加优雅的框架,至于如何建立这个角色,须要咱们从单个组件开始,从新梳理下应用形态,并着眼于:
一个组件的生态大概是这样的:
即:数据经处理造成页面状态,页面状态决定 UI 渲染。
而组件生态(UI + 状态 + 状态管理方式)的组合就构成了咱们应用:
这里组件生态特地只展现了数据到状态这一步,由于 Redux 处理的正是这个部分。咱们暂且能够定义数据到状态的过程为 flow,即一个业务流的意思。
借鉴于 Elm,咱们能够按数据模型对应用进行划分:
其中,模型具备的属性有:
name
: 模型名称state
:模型的初始状态reducers
:处理当前模型状态的 stateselectors
:服务于当前模型的 state selectorsflows
:当前模型涉及的业务流(反作用)这个经典的划分模型正是 Dva 的应用划分手段,只是模型属性略有不一样。
假定咱们建立了 user 模型和 post 模型,那么框架将挂载他们的状态到 user 和 post 状态子树下:
有了模型这个概念后,框架就能定义一系列的约定去减小样板代码的书写。首先,咱们回顾下之前咱们是怎么定义的一个 action type 的:
例如,咱们这样定义用户数据拉取相关的 action type:
const FETCH = 'USRE/FETCH'
const FETCH_SUCCESS = 'USER/FETCH_SUCCESSE'
const FETCH_ERROR = 'USER/FETCH_ERROR'
复制代码
其中, FETCH
对应的是一个异步 拉取数据的 action,FETCH_SUCCESS
和 FETCH_ERROR
则对应两个同步修改状态的 action。
同步 action 约定
对于同步的、不包含反作用的 action,咱们直接将其呈递到 reducer,是不会破坏 reducer 纯度的。 所以,咱们不妨约定: model 下 reducer 的名字映射一个直接对状态操做的 action type:
SYNC_ACTION_TYPE = MODEL_NAME/REDUCER_NAME
复制代码
例以下面这个 user model:
const userModel = {
name: 'user',
state: {
list: [],
total: 0,
loading: false
},
reducers: {
fetchStart(state, payload) {
return { ...state, loading:true }
}
}
}
复制代码
当咱们派发了一个类型为 user/fetchStart
的 action 以后,action 就带着其 payload 进入到 user.fetchStart
这个 reducer 下,进行状态变动。
异步 action 约定
对于异步的 action,咱们就不能直接在 reducer 进行异步任务处理,而 model 中的 flow 就是异步任务的集装箱:
ASYNC_ACTION_TYPE = MODEL_NAME/FLOW_NAME
复制代码
例以下面这个 model:
const user = {
name: 'user',
state: {
list: [],
total: 0,
loading: false
},
flows: {
fetch() {
// ... 处理一些异步任务
}
}
}
复制代码
若是咱们在 UI 里面发出了个 user/fetch
,因为 user model 中存在一个名为 fetch 的 flow,那么就进入到这个flow 中进行异步任务的处理。
状态的覆盖与更新
若是每一个状态的更新都去撰写一个对应的 reducer 就太累了,所以,咱们能够考虑为每一个模型定义一个 change reducer,用于直接更新状态:
const userModel = {
name: 'user',
state: {
list: [],
pagination: {
page: 1,
total: 0
},
loading: false
},
reducers: {
change(state, action) {
return { ...state, ...action.payload }
}
}
}
复制代码
此时,当咱们派发了下面的一个 action,就将可以将 loading
状态置为 true:
dispatch({
type: 'user/change',
payload: {
loading: true
}
})
复制代码
可是,这种更新是覆盖式的,假定咱们想要更新状态中的当前页面信息:
dispatch({
type: 'user/change',
payload: {
pagination: { page: 1 }
}
})
复制代码
状态就会变为:
{
list: [],
pagination: {
page: 1
},
loading: false
}
复制代码
pagination
状态被整个覆盖掉了,其中的总数状态 total
就丢失了。
所以,咱们还要定义一个 patch reducer,意为对状态的补丁更新,它只会影响到 action payload 中声明的子状态:
import { merge } from 'lodash.merge'
const userModel = {
name: 'user',
state: {
list: [],
pagination: {
page: 1,
total: 0
},
loading: false
},
reducers: {
change(state, action) {
return {
{ ...state, ...action.payload }
}
},
patch(state, action) {
return deepMerge(state, action.payload)
}
}
}
复制代码
如今,咱们尝试只更新分页:
dispatch({
type: 'user/patch',
payload: {
pagination: { page: 1 }
}
})
复制代码
新的状态就是:
{
list: [],
pagination: {
page: 1,
total: 0
},
loading: false
}
复制代码
注意:这里的实现不是生产环境的实现,直接使用 lodash 的 merge 是不够的,实际项目中还要进行必定改造。
Dva 使用了 redux-saga 进行反作用(主要是异步任务)的组织,Rematch 则使用了 async/await 进行组织。从长期的实践来看,我更偏向于使用 redux-observable,尤为是在其 1.0 版本的发布以后,更是带来了可观察的 state$
,使得咱们能更加透彻地实践响应式编程。咱们回顾下前文中提到的该模式的好处:
所以,对于模型异步任务的处理,咱们选择 redux-observable:
const user:Model<UserState> = {
name: 'user',
state: {
list: [],
// ...
},
reducers: {
// ...
},
flows: {
fetch(flow$, action$, state$) {
// ....
}
}
}
复制代码
与 epic 的函数签名略有不一样的是,每一个 flow 多了一个 flow$
参数,以上例来讲,它就至关于:
action$.ofType('user/fetch')
复制代码
这个参数便于咱们更快的取到须要的 action。
前端工程中常常会有错误展现和加载展现的需求,
若是咱们手动管理每一个模型的加载态和错误态就太麻烦了,所以在根状态下,单独划分两棵状态子树用于处理加载态与错误态,这样,便于框架去治理加载与错误,开发者直接在状态树上取用便可:
如图,加载态和错误态还须要根据粒度进行划分,有大粒度的 flow 级别,用于标识一个 flow 是否正在进行中;也有小粒度的 service 级别,用于标识某个异步服务是否在进行中。
例如,若:
loading.flows['user/fetch'] === true
复制代码
即表示 user model 下的 fetch
flow 正在进行中。
若:
loading.services['/api/fetchUser'] === true
复制代码
即表示 /api/fetchUser
这个服务正在进行中。
前端调用后端服务操纵数据是一个普遍的需求,所以,咱们还但愿所谓的中间角色(框架)可以在咱们的业务流中注入服务,完成服务和应用状态的交互:观察调用情况,自动捕获调用异常,适时地修改应用 loading 态和 error 态,方便用户直接在顶层状态取用服务运行情况。
另外,在响应式编程的范式下,框架提供的服务治理,在处理服务的成功和错误时应该也是响应式的,即成功和错误将是预约义的流(observable 对象),从而让开发者能更好的利用到响应式编程的能力:
const user:Model<UserState> = {
name: 'user',
state: {
list: [],
total: 0
},
reducers: {
fetchSuccess(state, payload) {
return { ...state, list: payload.list, total: payload.total }
},
fetchError(state, payload) {
return { ...state, list:[] }
}
},
flows: {
fetch(flow$, action$, state$, dependencies) {
const { service } = dependencies
return flow$.pipe(
withLatestFrom(state$, (action, state) => {
// 拼装请求参数
return params
}),
switchMap(params => {
const [success$, error$] = service(getUsers(params))
return merge(
success$.pipe(
map(resp => ({
type: 'user/fetchSuccess',
payload: {
list: resp.list,
total: resp.total
}
}))
),
error$.pipe(
map(error => ({
type: 'user/fetchError'
}))
)
)
})
)
}
}
}
复制代码
上面的种种思考,归纳下来其实就是 Dva architecture + redux-observable,前者可以打掉 Redux 冗长啰嗦的样板代码,后者则负责异步任务治理。
比较遗憾的是,Dva 没有使用 redux-observable 进行反作用管理,也没有相关插件实现使用 redux-observable 或者 RxJS 进行反作用管理,而且,经过 Dva 暴露的 hook 去实现一个 redux-observable 的 Dva 中间件也颇为不顺畅,所以,笔者尝试撰写了一个 reobservable 来实现上面提到框架,它与 Dva 不一样的是:
若是你的应用使用了 Redux,你苦于 Redux 种种负面影响,而且你仍是一个响应式编程和 RxJS 的爱好者,你能够尝试下 reobservable。可是若是你偏心 saga,或者 async await,你仍是应该选择 Dva 或者 Rematch,术业有专攻。