Epics

不熟悉 Observables/RxJS v5?

进入 redux-observable 之前需要理解 RxJS v5 的 Observables。 如果你是用 RxJS v5 进行响应式编程的新手, 转向 http://reactivex.io/rxjs/ 先熟悉下。

redux-observable (因为 RxJS) 的真正光芒在于处理最为复杂的异步逻辑。如果你对 RxJS 感到不太舒适,你可以考虑使用redux-thunk 处理简单异步逻辑,然后使用 redux-observable 处理复杂情况。这样既可以保持生产力又可以学习 RxJS。redux-thunk 学习和使用起来更简单,这也意味着它远没有那么强大。所以,如果你已经和我们一样喜爱 Rx,你可能会用它来做每一件事情。

Epic 是 redux-observable 的核心原语。

它是一个函数,接收 actions 流作为参数并且返回 actions 流。 Actions 入, actions 出.

它的签名如下:

  1. function (action$: Observable<Action>, store: Store): Observable<Action>;

虽然通常你会响应接收到的 action 而产出 actions,但这不是必须的!一旦进入你的 Epic,使用任何你想使用的 Observable 模式,只要最后返回 action 流即可。

你发出的 actions 会通过 store.dispatch() 立刻被分发,所以 redux-observable 实际上会做 epic(action$, store).subscribe(store.dispatch)

Epics 运行在正常的 Redux 分发通道旁,在 reducers 接受到之后,所以不会 “吞掉” 一个 action。
在 Epics 实际接收 Actions 前,Actions 将始终贯穿你的 reducers。

如果你传出 传入的 action ,会造成无限循环:

  1. // 不要这么做
  2. const actionEpic = (action$) => action$; // 创建无限循环

这种处理副作用的方式和”过程管理“模式相似,有些地方也称为saga,但是 saga 的
原始定义
并不适用。如果你熟悉 redux-saga, redux-observable 和它很像。但是因为它使用了 RxJS, 所以它是更加声明式的,同时还可以扩展你现有的
RxJS 能力。

一个基本例子

重要: redux-observable 并没有给 Observable.prototype 添加任何操作符,所以你需要在入口文件添加你使用的或者所有的操作符。

因为有很多种方式可以添加它们,我们的例子不会包含任何导入。如果你想添加所有的操作符,将 import 'rxjs'; 添加到你的入口 index.jsLearn more.

让我们从一个简单的 Epic 例子开始:

  1. const pingEpic = action$ =>
  2. action$.filter(action => action.type === 'PING')
  3. .mapTo({ type: 'PONG' });
  4. // 稍后...
  5. dispatch({ type: 'PING' });

注意,为什么 action$ 是以美元符结尾呢? 这是 RxJS 的基本公约用来标示流。

pingEpic 会监听类型为 PING 的 actions,然后投射为新的 action,PONG。这个例子功能上相当于做了这件事情:

  1. dispatch({ type: 'PING' });
  2. dispatch({ type: 'PONG' });

牢记: Epics 运行在正常分发渠道旁, 在 reducers 完全接受到它们之后。当你将一个 action 投射成另一个 action, 你不会 阻止原始的 action 到达 reducers; 该 action 已经通过了它!

真正的力量来自于你需要做一些异步事情。假设我们需要在接受到 PING 1秒后分发 PONG:

  1. const pingEpic = action$ =>
  2. action$.filter(action => action.type === 'PING')
  3. .delay(1000) // 异步等待 1000ms 然后继续
  4. .mapTo({ type: 'PONG' });
  5. // 稍后...
  6. dispatch({ type: 'PING' });

你的 reducers 会接收原始的 PING action,然后 1 秒后接收 PONG

  1. const pingReducer = (state = { isPinging: false }, action) => {
  2. switch (action.type) {
  3. case 'PING':
  4. return { isPinging: true };
  5. case 'PONG':
  6. return { isPinging: false };
  7. default:
  8. return state;
  9. }
  10. };

因为过滤特定的 action 类型是很常见的需求,action$ 流拥有 ofType() 操作符来减少这种复杂度。

  1. const pingEpic = action$ =>
  2. action$.ofType('PING')
  3. .delay(1000) // 异步等待 1000ms 然后继续
  4. .mapTo({ type: 'PONG' });

Try It Live!

View this demo on JSBin

来着真实世界的例子

现在我们对 Epic 是什么有了大概的了解,让我们继续,看下这个更加真实的例子:

  1. import { ajax } from 'rxjs/observable/dom/ajax';
  2. // action creators
  3. const fetchUser = username => ({ type: FETCH_USER, payload: username });
  4. const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });
  5. // epic
  6. const fetchUserEpic = action$ =>
  7. action$.ofType(FETCH_USER)
  8. .mergeMap(action =>
  9. ajax.getJSON(`https://api.github.com/users/${action.payload}`)
  10. .map(response => fetchUserFulfilled(response))
  11. );
  12. // 稍后...
  13. dispatch(fetchUser('torvalds'));

我们使用 fetchUser action 创建函数(工厂)代替直接创建 action POJO。这是一个完全可选的 Redux 惯例。

我们用个标准的 Redux action 创建者 fetchUser,同样也有一个对应的 Epic 去编排实际的 AJAX 调用。当 AJAX 调用返回时,我们将响应投射为 FETCH_USER_FULFILLED action。

记住,Epics 是一个 actions inactions out 的流。如果你发现 RxJS 操作符和行为如此陌生,在继续之前你应该深入看看 RxJS

FETCH_USER_FULFILLED action 的响应中,你可以修改你的 Store’s state。

  1. const users = (state = {}, action) => {
  2. switch (action.type) {
  3. case FETCH_USER_FULFILLED:
  4. return {
  5. ...state,
  6. // `login` is the username
  7. [action.payload.login]: action.payload
  8. };
  9. default:
  10. return state;
  11. }
  12. };

Try It Live!

View this demo on JSBin

访问 the Store’s State

你的 Epics 接收的第二个参数,一个轻量版的 Redux store。

  1. type LightStore = { getState: Function, dispatch: Function };
  2. function (action$: ActionsObservable<Action>, store: LightStore ): ActionsObservable<Action>;

这不是完整的 store 对象的引用,只包含了 store.getState()store.dispatch(); 它目前不支持Observable.from(store)

拥有它,你就可以调用 store.getState() 同步获取当前 state:

  1. const INCREMENT = 'INCREMENT';
  2. const INCREMENT_IF_ODD = 'INCREMENT_IF_ODD';
  3. const increment = () => ({ type: INCREMENT });
  4. const incrementIfOdd = () => ({ type: INCREMENT_IF_ODD });
  5. const incrementIfOddEpic = (action$, store) =>
  6. action$.ofType(INCREMENT_IF_ODD)
  7. .filter(() => store.getState().counter % 2 === 1)
  8. .map(() => increment());
  9. // later...
  10. dispatch(incrementIfOdd());

记住: 当 Epic 接收到 action, 它已经运行通过你的 reducers 并且 state 被修改了。

在 Epic 内部使用 store.dispatch() 是一个快速黑客的方便逃生舱,但是节制的使用。这被认为是反模式的并且会被在未来的版本中移除。


Try It Live!

View this demo on JSBinn

结合 Epics

最后,redux-observable 提供了一个工具方法 combineEpics(),该方法允许将多个 Epics 轻易的结合为一个:

  1. import { combineEpics } from 'redux-observable';
  2. const rootEpic = combineEpics(
  3. pingEpic,
  4. fetchUserEpic
  5. );

等价于:

  1. import { merge } from 'rxjs/observable/merge';
  2. const rootEpic = (action$, store) => merge(
  3. pingEpic(action$, store),
  4. fetchUserEpic(action$, store)
  5. );

下一步

接下来,我们会探索怎样 激活 Epics 才能开始监听 actions。