2.1 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 出.
它的签名如下:
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 ,会造成无限循环:
// 不要这么做
const actionEpic = (action$) => action$; // 创建无限循环
这种处理副作用的方式和"过程管理"模式相似,有些地方也称为"saga",但是 saga 的 原始定义并不适用。如果你熟悉 redux-saga, redux-observable 和它很像。但是因为它使用了 RxJS, 所以它是更加声明式的,同时还可以扩展你现有的 RxJS 能力。
一个基本例子
重要: redux-observable 并没有给
Observable.prototype
添加任何操作符,所以你需要在入口文件添加你使用的或者所有的操作符。因为有很多种方式可以添加它们,我们的例子不会包含任何导入。如果你想添加所有的操作符,将
import 'rxjs';
添加到你的入口index.js
。Learn more.
让我们从一个简单的 Epic 例子开始:
const pingEpic = action$ =>
action$.filter(action => action.type === 'PING')
.mapTo({ type: 'PONG' });
// 稍后...
dispatch({ type: 'PING' });
注意,为什么
action$
是以美元符结尾呢? 这是 RxJS 的基本公约用来标示流。
pingEpic
会监听类型为 PING
的 actions,然后投射为新的 action,PONG
。这个例子功能上相当于做了这件事情:
dispatch({ type: 'PING' });
dispatch({ type: 'PONG' });
牢记: Epics 运行在正常分发渠道旁, 在 reducers 完全接受到它们之后。当你将一个 action 投射成另一个 action, 你不会 阻止原始的 action 到达 reducers; 该 action 已经通过了它!
真正的力量来自于你需要做一些异步事情。假设我们需要在接受到 PING
1秒后分发 PONG
:
const pingEpic = action$ =>
action$.filter(action => action.type === 'PING')
.delay(1000) // 异步等待 1000ms 然后继续
.mapTo({ type: 'PONG' });
// 稍后...
dispatch({ type: 'PING' });
你的 reducers 会接收原始的 PING
action,然后 1 秒后接收 PONG
。
const pingReducer = (state = { isPinging: false }, action) => {
switch (action.type) {
case 'PING':
return { isPinging: true };
case 'PONG':
return { isPinging: false };
default:
return state;
}
};
因为过滤特定的 action 类型是很常见的需求,action$
流拥有 ofType()
操作符来减少这种复杂度。
const pingEpic = action$ =>
action$.ofType('PING')
.delay(1000) // 异步等待 1000ms 然后继续
.mapTo({ type: 'PONG' });
来着真实世界的例子
现在我们对 Epic 是什么有了大概的了解,让我们继续,看下这个更加真实的例子:
import { ajax } from 'rxjs/observable/dom/ajax';
// action creators
const fetchUser = username => ({ type: FETCH_USER, payload: username });
const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });
// epic
const fetchUserEpic = action$ =>
action$.ofType(FETCH_USER)
.mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`)
.map(response => fetchUserFulfilled(response))
);
// 稍后...
dispatch(fetchUser('torvalds'));
我们使用
fetchUser
action 创建函数(工厂)代替直接创建 action POJO。这是一个完全可选的 Redux 惯例。
我们用个标准的 Redux action 创建者 fetchUser
,同样也有一个对应的 Epic 去编排实际的 AJAX 调用。当 AJAX 调用返回时,我们将响应投射为 FETCH_USER_FULFILLED
action。
记住,Epics 是一个 actions in 和 actions out 的流。如果你发现 RxJS 操作符和行为如此陌生,在继续之前你应该深入看看 RxJS
在 FETCH_USER_FULFILLED
action 的响应中,你可以修改你的 Store's state。
const users = (state = {}, action) => {
switch (action.type) {
case FETCH_USER_FULFILLED:
return {
...state,
// `login` is the username
[action.payload.login]: action.payload
};
default:
return state;
}
};
访问 the Store's State
你的 Epics 接收的第二个参数,一个轻量版的 Redux store。
type LightStore = { getState: Function, dispatch: Function };
function (action$: ActionsObservable<Action>, store: LightStore ): ActionsObservable<Action>;
这不是完整的 store 对象的引用,只包含了
store.getState()
和store.dispatch()
; 它目前不支持Observable.from(store)
。
拥有它,你就可以调用 store.getState()
同步获取当前 state:
const INCREMENT = 'INCREMENT';
const INCREMENT_IF_ODD = 'INCREMENT_IF_ODD';
const increment = () => ({ type: INCREMENT });
const incrementIfOdd = () => ({ type: INCREMENT_IF_ODD });
const incrementIfOddEpic = (action$, store) =>
action$.ofType(INCREMENT_IF_ODD)
.filter(() => store.getState().counter % 2 === 1)
.map(() => increment());
// later...
dispatch(incrementIfOdd());
记住: 当 Epic 接收到 action, 它已经运行通过你的 reducers 并且 state 被修改了。
在 Epic 内部使用 store.dispatch()
是一个快速黑客的方便逃生舱,但是节制的使用。这被认为是反模式的并且会被在未来的版本中移除。
结合 Epics
最后,redux-observable 提供了一个工具方法 combineEpics()
,该方法允许将多个 Epics 轻易的结合为一个:
import { combineEpics } from 'redux-observable';
const rootEpic = combineEpics(
pingEpic,
fetchUserEpic
);
等价于:
import { merge } from 'rxjs/observable/merge';
const rootEpic = (action$, store) => merge(
pingEpic(action$, store),
fetchUserEpic(action$, store)
);
下一步
接下来,我们会探索怎样 激活 Epics 才能开始监听 actions。