在redux-observable中编写和排序多个史诗

时间:2018-01-15 10:49:18

标签: javascript redux rxjs redux-observable

我有一个问题,我不知道如何解决。

我有两个史诗要求api并更新商店:

git push

现在我有一个简单的场景,我想创建第三个动作,结合上面两个,让我们称之为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)); }); 。我该怎么做? 我认为在许多情况下(以及在我看来)重要的是第一个动作的结果在第二个动作开始之前被发送到商店。对于Promises和fetchCharacterAndPlanetEpic来说,这可能是微不足道的,但我无法想办法用redux-thunkrxjs来做到这一点。

谢谢!

2 个答案:

答案 0 :(得分:5)

Tomasz的回答有效并且有利有弊(最初在redux-observable#33中提出)。一个潜在的问题是它使测试更难,但并非不可能。例如你可能不得不使用依赖注入来注入分叉史诗的模拟。

在看到他之前我已经开始输入一个答案了,所以我想我也可以把它发给后人,以防任何人感兴趣。

我之前也回答过另一个非常相似但可能有用的问题: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检查主要针对您进行保护,因为如果它具有相同的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" action在元对象中保存唯一的事务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))
    );

此处描述的交易式解决方案真正的实验性。在实践中有一些瑕疵,多年来我已经注意到了,我还没有想过如何修复它们。这就是说,总的来说,它在我的应用程序中非常有用。实际上,它也可以用来做乐观的更新和回滚!几年前,我将这种模式和可选的乐观更新内容放入了库redux-transaction,但是我从来没有回过头来给它一些爱,所以使用风险自负 。它应被视为放弃,即使我可以回到它。

答案 1 :(得分:2)

我在github topic.找到了帮助 首先,我必须创建帮助方法,这将允许我将史诗组合在一起:

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))

...并将返回结果可观察到的史诗。通过这种方式,我可以将两个史诗结合在一起:

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结束时才会调用第二个请求。