在 redux-observable 中组合和排序多个 epics
Composing and sequencing multiple epics in redux-observable
我有一个不知道如何解决的问题。
我有两个 epics 请求 api 并更新商店:
const mapSuccess = actionType => response => ({
type: actionType + SUCCESS,
payload: response.response,
});
const mapFailure = actionType => error => Observable.of({
type: actionType + FAILURE,
error,
});
const characterEpic = (action$, store) =>
action$.ofType(GET_CHARACTER)
.mergeMap(({ id }) => {
return ajax(api.fetchCharacter(id))
.map(mapSuccess(GET_CHARACTER))
.catch(mapFailure(GET_CHARACTER));
});
const planetsEpic = (action$, store) =>
action$.ofType(GET_PLANET)
.mergeMap(({ id }) => {
return ajax(api.fetchPlanet(id))
.map(mapSuccess(GET_PLANET))
.catch(mapFailure(GET_PLANET));
});
现在我有一个简单的场景,我想创建结合以上两个的第三个动作,我们称之为 fetchCharacterAndPlanetEpic
。我该怎么做?
我认为在很多情况下(在我的情况下)重要的是第一个动作的结果在第二个动作开始之前发送到商店。对于 Promises 和 redux-thunk
,这可能是微不足道的,但我无法以某种方式想出一种方法来使用 rxjs
和 redux-observable
。
谢谢!
我在这方面找到了帮助 github topic.
首先,我必须创建辅助方法,让我可以将 epics 组合在一起:
import { ActionsObservable } from 'redux-observable';
const forkEpic = (epicFactory, store, ...actions) => {
const actions$ = ActionsObservable.of(...actions);
return epicFactory(actions$, store);
};
这让我可以调用任何带有存根操作的史诗,例如:
const getCharacter = id => ({ type: GET_CHARACTER, id });
forkEpic(getCharacterEpic, store, getCharacter(characterId))
...并且将 return 结果是该史诗的 Observable。这样我就可以将两个 epics 组合在一起:
export const getCharacterAndPlanet = (characterId, planetId) => ({
type: GET_CHARACTER_AND_PLANET,
characterId,
planetId,
});
const fetchCharacterAndPlanetEpic = (action$, store) =>
action$.ofType(GET_CHARACTER_AND_PLANET)
.mergeMap(({ characterId, planetId }) =>
forkEpic(characterEpic, store, getCharacter(characterId))
.mergeMap((action) => {
if (action.type.endsWith(SUCCESS)) {
return forkEpic(planetsEpic, store, getPlanet(planetId))
.startWith(action);
}
return Observable.of(action);
})
);
在此示例中,仅当第一个请求以 SUCCESS 结束时才调用第二个请求。
Tomasz 的答案有效且有利有弊(最初在 redux-observable#33 中建议)。一个潜在的问题是它使测试变得更加困难,但并非不可能。例如您可能必须使用依赖注入来注入分叉史诗的模拟。
我在看到他之前就已经开始输入答案了,所以我想我也可以 post 为了 post 真实,以防有人感兴趣。
我之前也回答过另一个非常相似的问题,可能会有帮助:
我们可以发出 getCharacter()
,然后在发出 getPlanet()
之前等待匹配的 GET_CHARACTER_SUCCESS
。
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
检查主要保护你免受这种情况的影响,因为如果它具有相同的 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))
);
关键细节是初始 "pend" 操作在元 object 中持有一个唯一的交易 ID。因此,初始操作基本上代表待处理的请求,然后在有人想要完成、拒绝或取消它时使用。 fulfill(action, payload)
我们的 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))
);
这里描述的 transaction-style 解决方案是 真正的实验性 。在实践中,多年来我注意到它有一些缺陷,但我还没有考虑如何修复它们。也就是说,总的来说它对我的应用程序很有帮助。事实上,它也可以用来做乐观更新和回滚呢!几年前,我将这个模式和可选的乐观更新内容放入库中 redux-transaction 但我从来没有回过头来给它一些爱,所以 使用风险自负 .应该算是放弃了,就算我可能会回来看。
我有一个不知道如何解决的问题。
我有两个 epics 请求 api 并更新商店:
const mapSuccess = actionType => response => ({
type: actionType + SUCCESS,
payload: response.response,
});
const mapFailure = actionType => error => Observable.of({
type: actionType + FAILURE,
error,
});
const characterEpic = (action$, store) =>
action$.ofType(GET_CHARACTER)
.mergeMap(({ id }) => {
return ajax(api.fetchCharacter(id))
.map(mapSuccess(GET_CHARACTER))
.catch(mapFailure(GET_CHARACTER));
});
const planetsEpic = (action$, store) =>
action$.ofType(GET_PLANET)
.mergeMap(({ id }) => {
return ajax(api.fetchPlanet(id))
.map(mapSuccess(GET_PLANET))
.catch(mapFailure(GET_PLANET));
});
现在我有一个简单的场景,我想创建结合以上两个的第三个动作,我们称之为 fetchCharacterAndPlanetEpic
。我该怎么做?
我认为在很多情况下(在我的情况下)重要的是第一个动作的结果在第二个动作开始之前发送到商店。对于 Promises 和 redux-thunk
,这可能是微不足道的,但我无法以某种方式想出一种方法来使用 rxjs
和 redux-observable
。
谢谢!
我在这方面找到了帮助 github topic. 首先,我必须创建辅助方法,让我可以将 epics 组合在一起:
import { ActionsObservable } from 'redux-observable';
const forkEpic = (epicFactory, store, ...actions) => {
const actions$ = ActionsObservable.of(...actions);
return epicFactory(actions$, store);
};
这让我可以调用任何带有存根操作的史诗,例如:
const getCharacter = id => ({ type: GET_CHARACTER, id });
forkEpic(getCharacterEpic, store, getCharacter(characterId))
...并且将 return 结果是该史诗的 Observable。这样我就可以将两个 epics 组合在一起:
export const getCharacterAndPlanet = (characterId, planetId) => ({
type: GET_CHARACTER_AND_PLANET,
characterId,
planetId,
});
const fetchCharacterAndPlanetEpic = (action$, store) =>
action$.ofType(GET_CHARACTER_AND_PLANET)
.mergeMap(({ characterId, planetId }) =>
forkEpic(characterEpic, store, getCharacter(characterId))
.mergeMap((action) => {
if (action.type.endsWith(SUCCESS)) {
return forkEpic(planetsEpic, store, getPlanet(planetId))
.startWith(action);
}
return Observable.of(action);
})
);
在此示例中,仅当第一个请求以 SUCCESS 结束时才调用第二个请求。
Tomasz 的答案有效且有利有弊(最初在 redux-observable#33 中建议)。一个潜在的问题是它使测试变得更加困难,但并非不可能。例如您可能必须使用依赖注入来注入分叉史诗的模拟。
我在看到他之前就已经开始输入答案了,所以我想我也可以 post 为了 post 真实,以防有人感兴趣。
我之前也回答过另一个非常相似的问题,可能会有帮助:
我们可以发出 getCharacter()
,然后在发出 getPlanet()
之前等待匹配的 GET_CHARACTER_SUCCESS
。
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
检查主要保护你免受这种情况的影响,因为如果它具有相同的 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))
);
关键细节是初始 "pend" 操作在元 object 中持有一个唯一的交易 ID。因此,初始操作基本上代表待处理的请求,然后在有人想要完成、拒绝或取消它时使用。 fulfill(action, payload)
我们的 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))
);
这里描述的 transaction-style 解决方案是 真正的实验性 。在实践中,多年来我注意到它有一些缺陷,但我还没有考虑如何修复它们。也就是说,总的来说它对我的应用程序很有帮助。事实上,它也可以用来做乐观更新和回滚呢!几年前,我将这个模式和可选的乐观更新内容放入库中 redux-transaction 但我从来没有回过头来给它一些爱,所以 使用风险自负 .应该算是放弃了,就算我可能会回来看。