在 React 中使用 Redux-observable & Redux-rx-creator 来隔离副作用

623 阅读2分钟

在 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

一篇文章总结redux、react-redux、redux-saga

ngrx 官网

Redux-observable 官网