在 React 中使用 Redux-observable & Redux-rx-creator 来隔离副作用
在开发 React 应用的过程中,我们一般习惯使用 Redux 作为状态管理工具,因为这个工具足够的简单。而在一般的项目中,Redux通过中间件,提供了足够的能力处理同步异步事件。而处理异步事件的中间件有很多,Redux-observable 就是一个,它通过和 Rxjs 深度结合,提供了一种很棒的方式来处理异步事件以及副作用。
Redux-observable(RO)
如果你了解过 NgRx,那么,下面的介绍的方法您肯定不会陌生,首先,我容我“盗用” NgRx 的状态图。

为了解决 Side-Effect,大家都想到很多的方法:
-
Redux-thunk 通过中间件增强了dispatch方法,使得其输入可以包括函数对象,通过输入函数,我们可以在函数中引入副作用,并对其进行处理。然而,action 和 effect 糅杂在一起,并不能很好的管理副作用。同时,action 的数据形式受到了一定程度的破坏,在我看来,action应该是一个纯粹的 plain object,而不是函数对象。
-
Redux-sega 则是更接近与 Redux-observable 的一种解决方式。使用观察者模式可以对输入的action进行观察,将 Effect 隔离出来。通过 Generator 函数,并将异步操作(代码)同步化。由于是同步操作,可以通过 try/catch 进行错误处理。
RO 跟 Redux-sega 的操作思想类似,但是 RO 引入了我比较喜欢的 Rxjs,通过 Rxjs 的流概念带来的强大的抽象能力,管理副作用变得轻而易举,使得 RO 在管理数据流方面有着天然的优势。
具体写法
在 RO 有这么一个概念,叫 Epic。在 RO 的官网上,是这样描述它的:
It is a function which takes a stream of actions and returns a stream of actions. Actions in, actions out.
简而言之,Epic就是一个action操作流的函数。这个函数的签名是这样的:
function (action$: Observable<Action>, state$: StateObservable<State>): Observable<Action>;
Epic 操作流的的方式是这样的(直接用了官网的例子了):
const pingEpic = action$ => action$.pipe(
filter(action => action.type === 'PING'),
mapTo({ type: 'PONG' })
);
可以用 ofType() 这个操作符将特定 action 过滤:
const fetchAction
const fetchUserEpic = action$ => action$.pipe(
ofType(FETCH_USER),
mergeMap(action => ajax.getJSON(`https://api.github.com/users/${action.payload}`)),
map(response => fetchUserFulfilled(response)))
);
在 Epic 的基础上,RO 引申出了 Root Epic 这个概念,Root Epic 包含了所有的 Epic。每一个 Epic 通过一个 merge 操作符 而它最终会被 epicMiddleware 这个中间件调用。
const rootEpic = combineEpics(
pingEpic,
fetchUserEpic
);
// ... 此处省略 rootReducer
const epicMiddleware = createEpicMiddleware();
export default function configureStore() {
const store = createStore(
rootReducer,
applyMiddleware(epicMiddleware)
);
epicMiddleware.run(rootEpic);
return store;
}
Redux-rx-creator
这个库是将 NgRx 中的 action 和 reducer 工厂函数抽离出来的一个库。
Reducer creator
这个库也提供了一个 createReducer 的函数,为了搭配 createReducer,你可能需要简单的改造一下你的 state。
// 为 state 提供类型
interface PingPongState {
pingCount: number;
pongCount: number;
}
const initPingPongState: PingPongState = {
pingCount: 0,
pongCount: 0
}
const pingPongReducer = createReducer(
initState,
on(ping, state => ({...state, pingCount: state.pingCount + 1})),
on(pong, state => ({...state, pongCount: state.pongCount + 1}))
);
这种方式,比 Redux 常用创建 Reducer 的方式看起来更加的舒畅,它仅仅关注与数据的变化,而不是复杂的 switch 结构,这使得代码更加的清晰。最重要的一点,createReducer
的返回值就是一个 reducer 函数,这也就意味着,你可以直接使用 combineReducer
来做结合。
Action creator
一般来说,action 是一个 plain object。为了能够在使用 typescript 开发过程中提供完整的类型支持,这个 action 工厂函数可以提供明显且有效的帮助。
const doPing = createAction('PING');
如果这个 action 需要传入参数,那么就可以使用这种方式为 action 提供类型检测。
const fetchUser = createAction('FETCH_USER', props<{payload: string}>());
在这里 props 仅仅返回了 undefined,不会造成任何影响,但是却给 fetchUser 提供类型检测的机会。fetchUser 的类型是一个函数(也就是所谓的高阶 action),它接受一个参数,这个参数的类型来自于 props 中的泛型接口。那么,当你需要调用这个 fetchUser 的时候,仅仅需要这样。
store.dispatch(fetchUser({payload: 'Tony'}));
ofActionType 操作符
为了可以结合 RO,我们需要使用这个操作符来强化类型推导,如果直接使用 RO 自带的 ofType,那么会有这种情况发生。
const fetchUserEpic = action$ => action$.pipe(
ofType(fetchUser),
// 此处 typescript 会报错,无法转发出正确的类型,导致 action.payload 失效。
mergeMap(action => {
return ajax.getJSON(`https://api.github.com/users/${action.payload}`));
}
map(response => fetchUserFulfilled(response)))
);
因此我们需要使用这个 ofActionType 操作符,来将 ActionCreator 中的数据类型取出。
const fetchUserEpic = action$ => action$.pipe(
ofActionType(fetchUser),
mergeMap(action => {
return ajax.getJSON(`https://api.github.com/users/${action.payload}`));
}
map(response => fetchUserFulfilled(response)))
);
总结
其实,我就是把 NgRx 中一些有用的地方应用到 Redux,主要就是解决了创建 action 类型安全的问题。本文参考如下。
Vuex、Flux、Redux、Redux-saga、Dva、MobX