内容简介:首先要明确的是,Redux 并不是 React 独有的一个插件,它是顺应前端组件化开发潮流而诞生的一种状态管理模型,你在 Vue 或者 Angular 中也可以使用这个模型。目前,大家都比较认可的是,某一时刻的应用或者组件状态,将对应此时应用或者组件的 UI:那么,在前端组件化开发的时候,就需要思考两个问题:
首先要明确的是,Redux 并不是 React 独有的一个插件,它是顺应前端组件化开发潮流而诞生的一种状态管理模型,你在 Vue 或者 Angular 中也可以使用这个模型。
目前,大家都比较认可的是,某一时刻的应用或者组件状态,将对应此时应用或者组件的 UI:
UI = f(state) 复制代码
那么,在前端组件化开发的时候,就需要思考两个问题:
- 状态来源
- 状态管理
组件所具有的状态,一搬来源于两个方面:
- 自身具有的状态 :例如一个 Button 组件自身含有一个计数状态 count,表示自己被点击的次数。
- 外部注入的状态 :例如一个 Modal 组件,就需要由外部注入一个是否显示的状态 visible。React 将外部注入的状态称为 props 。
状态源为组件输送了其需要的状态,进而,组件的外观形态也得到了确认。在简单工程和简单组件中,我们思考了状态来源也就行了,如果引入额外的状态管理方案(例如我们为一个使用 Redux 管理一个按钮组件的状态),反而会加重每个组件的负担,造成了多余的抽象和依赖。
而对于 大型前端工程 和复杂组件来说,其往往具有如下特点:
- 数据复杂
- 组件丰富
在这种场景下,朴素的状态管理就显得捉襟见肘了,主要体现在下面几个方面:
- 当组件 层级过深 时,如何优雅得呈递组件需要的状态,或者说组件如何更方便取得自己需要的状态
- 如何 回溯 到某个状态
- 如何更好的 测试 状态管理
Redux 正是要去解决这些问题,从而让大型前端工程的状态更加可控。Redux 提出了一套约定模型,让状态的更新和派发都集中了:
Redux 所使用的模型是受到了 Elm 的启发:
在 Elm 中,流动于应用中的是 消息(msg) :一个由**消息类型(type) 所标识,并且携带了 内容(payload)**的数据结构。消息决定了数据模型( model )怎么更新,而数据又决定了 UI 形态。
而在 Redux 中,消息被称替代为 动作(action) ,并且使用 reducer 来描述状态随行为的变迁。另外,与 Elm 不同的是,Redux 专注于状态管理,而不再处理视图(View),因此 ,Redux 也不是分型的(关于分型架构的介绍,可以看 的博文)。
在了解到 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} } } } 复制代码
这个流程带来的问题是:
- 个人开发不够专注 :工程中,我们是 分散管理 action type、action 及 reducer 的,走完一套流程,需要在当中不停的跳跃,思路不够集中。
- 多人协作不够高效 :同样是因为 action type、action 及 reducer 的分散,多人协作时就会出现名字冲突,相似业务的流程重复等问题。这对我们的应用状态设计提出了比较高的要求。优秀的设计是状态易于定位,变迁流程清晰,无冗余状态,而低下的设计就会让状态膨胀难于定位,变迁流程错综复杂,冗余状态随处可见。
怎么用好 Redux
当我们受困于 Redux 的负面影响时,切到其他的状态管理方案(例如 mobx 或者 mobx-state-stree) ,也不太现实,一方面是迁移成本大,一方面你也不知道新的状态管理方案是否就是银弹。但是,对 Redux 的负面影响无动于衷或者忍气吞声,也只会让问题越滚越大,直到失控。
在开始讨论如何更好地 Redux 之前,我们需要明确一点,样板代码和异步能力的缺乏, 是 Redux 自身设计的结果,而非目的 ,换句话说,Redux 设计出来,并不是要让开发者去撰写样本代码,或者去纠结怎么处理异步状态更新。
我们需要再定义一个角色,让他来代替我们去写样板代码,让他给予我们最优秀的异步任务处理能力,让他负责一切 Redux 中恶心的事儿。因此,这个角色就是一个让 Redux 变得更加优雅的框架,至于如何创建这个角色,需要我们从单个组件开始,重新梳理下应用形态,并着眼于:
- 如何打掉 Redux 的样板代码
- 如何更优雅地处理异步任务
组件的样子
一个组件的生态大概是这样的:
即:数据经处理形成页面状态,页面状态决定 UI 渲染。
应用的样子
而组件生态(UI + 状态 + 状态管理方式)的组合就构成了我们应用:
这里组件生态特意只展示了 数据到状态 这一步,因为 Redux 处理的正是这个部分。我们暂且可以定义数据到状态的过程为 flow ,即一个业务流的意思。
应用划分
借鉴于 Elm,我们可以按数据模型对应用进行划分:
其中,模型具有的属性有:
name state reducers selectors flows
这个经典的划分模型正是 Dva 的应用划分手段,只是模型属性略有不同。
假定我们创建了 user 模型和 post 模型,那么框架将挂载他们的状态到 user 和 post 状态子树下:
约定 —— 打掉样板代码
有了模型这个概念后,框架就能定义一系列的约定去减少样板代码的书写。首先,我们回顾下以前我们是怎么定义的一个 action type 的:
- action 名称
- 指定一个 namespace 防止名字冲突
例如,我们这样定义用户数据拉取相关的 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$
,使得我们能更加透彻地实践响应式编程。我们回顾下前文中提到的该模式的好处:
- 统一数据源,observable 之间可组合
- 声明式编程,代码直爽简洁
- 优秀的竞态处理能力
- 测试友好
- 便于实现组件自治
因此,对于模型异步任务的处理,我们选择 redux-observable:
const user:Model<UserState> = { name: 'user', state: { list: [], // ... }, reducers: { // ... }, flows: { fetch(flow$, action$, state$) { // .... } } } 复制代码
与 epic 的函数签名略有不同的是,每个 flow 多了一个 flow$
参数,以上例来说,它就相当于:
action$.ofType('user/fetch') 复制代码
这个参数便于我们更快的取到需要的 action。
处理加载态与错误态
前端工程中经常会有错误展示和加载展示的需求,
如果我们手动管理每个模型的加载态和错误态就太麻烦了,因此在根状态下,单独划分两棵状态子树用于处理加载态与错误态,这样,便于框架去治理加载与错误,开发者直接在状态树上取用即可:
- loading
- error
如图,加载态和错误态还需要根据粒度进行划分,有大粒度的 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' })) ) ) }) ) } } } 复制代码
reobservable
上面的种种思考,概括下来其实就是 Dva architecture + redux-observable,前者能够打掉 Redux 冗长啰嗦的样板代码,后者则负责异步任务治理。
比较遗憾的是,Dva 没有使用 redux-observable 进行副作用管理,也没有相关插件实现使用 redux-observable 或者 RxJS 进行副作用管理,并且,通过 Dva 暴露的 hook 去实现一个 redux-observable 的 Dva 中间件也颇为不畅,因此,笔者尝试撰写了一个 reobservable 来实现上面提到框架,它与 Dva 不同的是:
- 只关注应用状态,不涉及组件路由的其他生态
- 集成 loading 和 error 处理
- 使用 redux-observable 而不是 redux-saga 处理副作用
- 响应式的服务处理,支持应用自定义服务细节
如果你的应用使用了 Redux,你苦于 Redux 种种负面影响,并且你还是一个响应式编程和 RxJS 的爱好者,你可以尝试下 reobservable。但是如果你偏爱 saga,或者 async await,你还是应该选择 Dva 或者 Rematch,术业有专攻。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
C# Primer Plus
Klaus Michelsen / Sams / 2001-12-15 / USD 49.99
C# Primer Plus is a tutorial based introduction to the C# language and important parts of the .Net Framework. Throughout the book the reader will be exposed to proven principles enabling him to write ......一起来看看 《C# Primer Plus》 这本书的介绍吧!