Tomasz的答案有效并且有优点和缺点(最初是在redux-observable#33 https://github.com/redux-observable/redux-observable/issues/33#issuecomment-239329023)。一个潜在的问题是它使测试变得更加困难,但并非不可能。例如您可能必须使用依赖项注入来注入分叉史诗的模拟。
在看到他之前我已经开始输入答案了,所以我想我不妨将其发布给后代,以防任何人都感兴趣。
我之前也回答过另一个非常相似的问题,可能会有所帮助:如何延迟一个史诗直到另一个史诗发出值 https://stackoverflow.com/questions/42426908/how-to-delay-one-epic-until-another-has-emitted-a-value
我们可以发出getCharacter()
,然后等待匹配GET_CHARACTER_SUCCESS
在我们发出之前getPlanet()
.
const fetchCharacterAndPlanetEpic = (action$, store) =>
action$.ofType(GET_CHARACTER_AND_PLANET)
.mergeMap(({ characterId, planetId }) =>
action$.ofType(GET_CHARACTER_SUCCESS)
.filter(action => action.payload.id === characterId) // just in case
.take(1)
.mapTo(getPlanet(planetId))
.startWith(getCharacter(characterId))
);
这种方法的一个潜在的负面影响是,理论上GET_CHARACTER_SUCCESS
这部史诗般的作品可能与我们等待的完全不同。过滤器action.payload.id === characterId
check 可以最大程度地保护您免受这种情况的影响,因为如果它具有相同的 ID,那么它是否属于您可能并不重要。
要真正解决这个问题,您需要某种独特的交易跟踪。我个人使用自定义解决方案,其中涉及使用辅助函数来包含唯一的事务 ID。像这样的东西:
let transactionID = 0;
const pend = action => ({
...action,
meta: {
transaction: {
type: BEGIN,
id: `${++transactionID}`
}
}
});
const fulfill = (action, payload) => ({
type: action.type + '_FULFILLED',
payload,
meta: {
transaction: {
type: COMMIT,
id: action.meta.transaction.id
}
}
});
const selectTransaction = action => action.meta.transaction;
然后它们可以像这样使用:
const getCharacter = id => pend({ type: GET_CHARACTER, id });
const characterEpic = (action$, store) =>
action$.ofType(GET_CHARACTER)
.mergeMap(action => {
return ajax(api.fetchCharacter(action.id))
.map(response => fulfill(action, payload))
.catch(e => Observable.of(reject(action, e)));
});
const fetchCharacterAndPlanetEpic = (action$, store) =>
action$.ofType(GET_CHARACTER_AND_PLANET)
.mergeMap(action =>
action$.ofType(GET_CHARACTER_FULFILLED)
.filter(responseAction => selectTransaction(action).id === selectTransaction(responseAction).id)
.take(1)
.mapTo(getPlanet(action.planetId))
.startWith(getCharacter(action.characterId))
);
关键细节是初始“挂起”操作在元对象中保存唯一的事务 ID。因此,初始操作基本上代表待处理的请求,然后在有人想要履行、拒绝或取消它时使用。fulfill(action, payload)
Our fetchCharacterAndPlanetEpic
代码有点冗长,如果我们使用这样的东西,我们会做很多事情。因此,让我们创建一个自定义运算符来为我们处理这一切。
// Extend ActionsObservable so we can have our own custom operators.
// In rxjs v6 you won't need to do this as it uses "pipeable" aka "lettable"
// operators instead of using prototype-based methods.
// https://github.com/ReactiveX/rxjs/blob/master/doc/pipeable-operators.md
class MyCustomActionsObservable extends ActionsObservable {
takeFulfilledTransaction(input) {
return this
.filter(output =>
output.type === input.type + '_FULFILLED' &&
output.meta.transaction.id === input.meta.transaction.id
)
.take(1);
}
}
// Use our custom ActionsObservable
const adapter = {
input: input$ => new MyCustomActionsObservable(input$),
output: output$ => output$
};
const epicMiddleware = createEpicMiddleware(rootEpic, { adapter });
然后我们可以在我们的史诗中干净利落地使用该自定义运算符
const fetchCharacterAndPlanetEpic = (action$, store) =>
action$.ofType(GET_CHARACTER_AND_PLANET)
.mergeMap(action =>
action$.takeFulfilledTransaction(action)
.mapTo(getPlanet(action.planetId))
.startWith(getCharacter(action.characterId))
);
这里描述的事务式解决方案是真正的实验性。在实践中,多年来我注意到它有一些缺点,但我还没有抽出时间去考虑如何解决它们。也就是说,总的来说,它对我的应用程序非常有帮助。事实上,它也可以用来做乐观更新和回滚!几年前,我将这个模式和可选的乐观更新内容放入库中redux-事务 https://github.com/jayphelps/redux-transaction但我从来没有回过头来给予它一些爱,所以使用风险自负。即使我可能会回来,它也应该被视为放弃。