Redux异步方案选型
作为react社区最热门的状态管理框架,相信很多人都准备甚至正在使用Redux。 由于Redux的理念非常精简,没有追求大而全,这份架构上的优雅却在某种程度上伤害了使用体验:不能开箱即用,甚至是异步这种最常见的场景也要借助社区方案。 如果你已经挑花了眼,或者正在挑但不知道是否适合,或者已经挑了但不知道会不会有坑,这篇文章应该适合你。 本文会从一些常见的Redux异步方案出发,介绍它们的优缺点,进而讨论一些与异步相伴的常见场景,帮助你在选型时更好地权衡利弊。 简单方案redux-thunk:指路先驱Github:https://github.com/gaearon/redux-thunk Redux作者Dan写的中间件,因官方文档出镜而广为人知。 它向我们展示了Redux处理异步的原理,即:
而它使用起来最大的问题,就是重复的模板代码太多: //action types const GET_DATA = 'GET_DATA',GET_DATA_SUCCESS = 'GET_DATA_SUCCESS',GET_DATA_FAILED = 'GET_DATA_FAILED'; //action creator const getDataAction = function(id) { return function(dispatch,getState) { dispatch({ type: GET_DATA,payload: id }) api.getData(id) //注:本文所有示例的api.getData都返回promise对象 .then(response => { dispatch({ type: GET_DATA_SUCCESS,payload: response }) }) .catch(error => { dispatch({ type: GET_DATA_FAILED,payload: error }) }) } } //reducer const reducer = function(oldState,action) { switch(action.type) { case GET_DATA : return oldState; case GET_DATA_SUCCESS : return successState; case GET_DATA_FAILED : return errorState; } } 这已经是最简单的场景了,请注意:我们甚至还没写一行业务逻辑,如果每个异步处理都像这样,重复且无意义的工作会变成明显的阻碍。 另一方面,像 上例中, redux-promise:瘦身过头由于 它自定义了一个middleware,当检测到有action的payload属性是Promise对象时,就会:
说起来可能有点不好理解,用代码感受下: //action types const GET_DATA = 'GET_DATA'; //action creator const getData = function(id) { return { type: GET_DATA,payload: api.getData(id) //payload为promise对象 } } //reducer function reducer(oldState,action) { switch(action.type) { case GET_DATA: if (action.status === 'success') { return successState } else { return errorState } } } 进步巨大! 代码量明显减少! 就用它了! ? 请等等,任何能明显减少代码量的方案,都应该小心它是否过度省略了什么东西,减肥是好事,减到骨头就残了。 redux-promise为了精简而做出的妥协非常明显:无法处理乐观更新 场景解析之:乐观更新多数异步场景都是 最常见的例子就是微信等聊天工具,发送消息时消息立即进入了对话窗,如果发送失败的话,在消息旁边再作补充提示即可。这种交互"乐观"地相信请求会成功,因此称作 由于 在上面redux-thunk的例子中,我们看到了 而在redux-promise中,最初触发的action被中间件拦截然后过滤掉了。原因很简单,redux认可的action对象是 plain JavaScript objects,即简单对象,而在redux-promise中,初始action的payload是个Promise。 另一方面,使用 redux-promise-middleware:拔乱反正redux-promise-middleware相比redux-promise,采取了更为温和和渐进式的思路,保留了和redux-thunk类似的三个action。 示例: //action types const GET_DATA = 'GET_DATA',GET_DATA_PENDING = 'GET_DATA_PENDING',GET_DATA_FULFILLED = 'GET_DATA_FULFILLED',GET_DATA_REJECTED = 'GET_DATA_REJECTED'; //action creator const getData = function(id) { return { type: GET_DATA,payload: { promise: api.getData(id),data: id } } } //reducer const reducer = function(oldState,action) { switch(action.type) { case GET_DATA_PENDING : return oldState; // 可通过action.payload.data获取id case GET_DATA_FULFILLED : return successState; case GET_DATA_REJECTED : return errorState; } } 如果不需要乐观更新,action creator可以使用和redux-promise完全一样的,更简洁的写法,即: const getData = function(id) { return { type: GET_DATA,payload: api.getData(id) //等价于 {promise: api.getData(id)} } } 此时初始action 相对redux-promise于粗暴地过滤掉整个初始action,redux-promise-middleware选择创建一个只过滤payload中的promise属性的 同时在action的区分上,它选择了回归 它的遗憾则是只在action层实现了简化,对reducer层则束手无策。另外,相比redux-thunk,它还多出了一个
redux-action-tools:软文预警无论是 国外开发者也有相同的报怨:
有没有办法让代码既像redux-promise一样简洁,又能保持乐观更新的能力呢? redux-action-tools是我给出的答案: const GET_DATA = 'GET_DATA'; //action creator const getData = createAsyncAction(GET_DATA,function(id) { return api.getData(id) }) //reducer const reducer = createReducer() .when(getData,(oldState,action) => oldState) .done((oldState,action) => successState) .failed((oldState,action) => errorState) .build() redux-action-tools在action层面做的事情与前面几个库大同小异:同样是派发了三个action:
目前看来,其实和redux-promise/redux-promise-middleware大同小异。而真正不同的,是它同时简化了reducer层! 这种简化来自于对异步行为从语义角度的抽象:
抽离出 无论是action还是reducer层, 更进一步的,这三个action默认都根据当前所处的异步阶段,设置了不同的meta(见上表中的meta.asyncPhase),它有什么用呢?用场景说话: 场景解析:失败处理与Loading它们是异步不可回避的两个场景,几乎每个项目会遇到。 以异步请求的失败处理为例,每个项目通常都有一套比较通用的,适合多数场景的处理逻辑,比如弹窗提示。同时在一些特定场景下,又需要绕过通用逻辑进行单独处理,比如表单的异步校验。 而在实现通用处理逻辑时,常见的问题有以下几种:
通过以上几种常见方案的分析,我认为比较完善的错误处理(Loading同理)需要具备如下特点:
而借助 import _ from 'lodash' import { ASYNC_PHASES } from 'redux-action-tools' function errorMiddleWare({dispatch}) { return next => action => { const asyncStep = _.get(action,'meta.asyncStep'); if (asyncStep === ASYNC_PHASES.FAILED) { dispatch({ type: 'COMMON_ERROR',payload: { action } }) } next(action); } } 以上中间件一旦检测到 import _ from 'lodash' import { ASYNC_PHASES } from 'redux-action-tools' const customizedAction = createAsyncAction( type,promiseCreator,//type 和 promiseCreator此处无不同故省略 (payload,defaultMeta) => { return { ...defaultMeta,omitError: true }; //向meta中添加配置参数 } ) function errorMiddleWare({dispatch}) { return next => action => { const asyncStep = _.get(action,'meta.asyncStep'); const omitError = _.get(action,'meta.omitError'); //获取配置参数 if (!omitError && asyncStep === ASYNC_PHASES.FAILED) { dispatch({ type: 'COMMON_ERROR',payload: { action } }) } next(action); } } 类似的,你可以想想如何处理Loading,需要强调的是建议尽量用增量配置的方式进行扩展,而不要轻易删除和修改meta.asyncPhase。 比如上例可以通过删除 进阶方案上面所有的方案,都把异步请求这一动作放在了action creator中,这样做的好处是简单直观,且和Flux社区一脉相承(见下图)。因此个人将它们归为相对简单的一类。
下面将要介绍的,是相对复杂一类,它们都采用了与上图不同的思路,去追求更优雅的架构、解决更复杂的问题 redux-loop:分形! 组合!众所周知,Redux是借鉴自Elm的,然而在Elm中,异步的处理却并不是在action creator层,而是在reducer(Elm中称update)层:
这样做的目的是为了实现彻底的可组合性(composable)。在redux中,reducer作为函数是可组合的,action正常情况下作为纯对象也是可组合的,然而一旦涉及异步,当action嵌套组合的时候,中间件就无法正常识别,这个问题让redux作者Dan也发出感叹 There is no easy way to compose Redux applications并且开了一个至今仍然open的issue,对组合、分形与redux的故事,有兴趣的朋友可以观摩以上链接,甚至了解一下Elm,篇幅所限,本文难以尽述。 而redux-loop,则是在这方面的一个尝试,它更彻底的模仿了Elm的模式:引入Effects的概念并将其置入reducer,官方示例如下: import { Effects,loop } from 'redux-loop'; import { loadingStart,loadingSuccess,loadingFailure } from './actions'; export function fetchDetails(id) { return fetch(`/api/details/${id}`) .then((r) => r.json()) .then(loadingSuccess) .catch(loadingFailure); } export default function reducer(state,action) { switch (action.type) { case 'LOADING_START': return loop( { ...state,loading: true },Effects.promise(fetchDetails,action.payload.id) ); // 同时返回状态与副作用 case 'LOADING_SUCCESS': return { ...state,loading: false,details: action.payload }; case 'LOADING_FAILURE': return { ...state,error: action.payload.message }; default: return state; } } 注意在reducer中,当处理 然而修改reducer的返回类型显然是比较暴力的做法,除非Redux官方出面,否则很难获得社区的广泛认同。更复杂的返回类型会让很多已有的API,三方库面临危险,甚至 对Elm的分形架构有了解,想在Redux上继续实践的人来说,redux-loop是很好的参考素材,但对多数人和项目而言,最好还是更谨慎地看待。 redux-saga:难、而美Github: https://github.com/yelouafi/r... 另一个著名的库,它让异步行为成为架构中独立的一层(称为saga),既不在action creator中,也不和reducer沾边。 它的出发点是把副作用 (Side effect,异步行为就是典型的副作用) 看成"线程",可以通过普通的action去触发它,当副作用完成时也会触发action作为输出。 import { takeEvery } from 'redux-saga' import { call,put } from 'redux-saga/effects' import Api from '...' function* getData(action) { try { const response = yield call(api.getData,action.payload.id); yield put({type: "GET_DATA_SUCCEEDED",payload: response}); } catch (e) { yield put({type: "GET_DATA_FAILED",payload: error}); } } function* mySaga() { yield* takeEvery("GET_DATA",getData); } export default mySaga; 相比action creator的方案,它可以保证组件触发的action是纯对象,因此至少在项目范围内(middleware和saga都是项目的顶层依赖,跨项目无法保证),action的组合性明显更加优秀。 而它最为主打的,则是可测试性和强大的异步流程控制。 由于强制所有saga都必须是generator函数,借助generator的next接口,异步行为的每个中间步骤都被暴露给了开发者,从而实现对异步逻辑"step by step"的测试。这在其它方案中是很少看到的 (当然也可以借鉴generator这一点,但缺少约束)。 而强大得有点眼花缭乱的API,特别是channel的引入,则提供了武装到牙齿级的异步流程控制能力。 然而,回顾我们在讨论简单方案时提到的各种场景与问题,redux-saga并没有去尝试回答和解决它们,这意味着你需要自行寻找解决方案。而generator、相对复杂的API和单独的一层抽象也让不少人望而却步。 包括我在内,很多人非常欣赏redux-saga。它的架构和思路毫无疑问是优秀甚至优雅的,但使用它之前,最好想清楚它带来的优点(可测试性、流程控制、高度解耦)与付出的成本是否匹配,特别是异步方面复杂度并不高的项目,比如多数以CRUD为主的管理系统。 场景解析:竞态说到异步流程控制很多人可能觉得太抽象,这里举个简单的例子:竞态。这个问题并不罕见,知乎也有见到类似问题。 简单描述为:
这在redux-thunk为代表的简单方案中是要费点功夫的: function fetchFriend(id){ return (dispatch,getState) => { //步骤1:在reducer中 set state.currentFriend = id; dispatch({type: 'FETCH_FIREND',payload: id}); return fetch(`http://localhost/api/firend/${id}`) .then(response => response.json()) .then(json => { //步骤2:只处理currentFriend的对应response const { currentFriend } = getState(); (currentFriend === id) && dispatch({type: 'RECEIVE_FIRENDS',playload: json}) }); } } 以上只是示例,实际中不一定需要依赖业务id,也不一定要把id存到store里,只要为每个请求生成key,以便处理请求时能够对应起来即可。 而在redux-saga中,一切非常地简单: import { takeLatest } from `redux-saga` function* fetchFriend(action) { ... } function* watchLastFetchUser() { yield takeLatest('FETCH_FIREND',fetchFriend) } 这里的重点是takeLatest,它限制了同步事件与异步返回事件的顺序关系。 另外还有一些基于响应式编程(Reactive Programming)的异步方案(如redux-observable)也能非常好地处理竞态场景,因为描述事件流之间的关系,正是整个响应式编程的抽象基石,而竞态在本质上就是如何保证同步事件与异步返回事件的关系,正是响应式编程的用武之地。 小结本文包含了一些redux社区著名、非著名 (恩,我的redux-action-tools) 的异步方案,这些其实并不重要。 因为方案是一家之作,结论也是一家之言,不可能放之四海皆准。个人更希望文中探讨过的常见问题和场景,比如模板代码、乐观更新、错误处理、竞态等,能够成为你选型时的尺子,为你的权衡提供更好的参考,而不是等到项目热火朝天的时候,才发现当初选型的硬伤。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |