撰写和测序多个史诗终极版可观测

Tom*_*zyk 2 javascript rxjs redux redux-observable

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

我有两个史诗,可以请求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));
    });
Run Code Online (Sandbox Code Playgroud)

现在,我有一个简单的场景,在该场景中,我想创建结合了以上两者的第三个动作,我们称之为fetchCharacterAndPlanetEpic。我该怎么做?我认为在很多情况下(对我而言),重要的是在第二个操作开始之前将第一个操作的结果发送到商店。这与Promises和可能是微不足道的redux-thunk,但是我无法以某种方式想出rxjs和and 一起实现的方法redux-observable

谢谢!

jay*_*lps 9

托马斯(Tomasz)的答案行之有效,也各有利弊(最初是在redux-observable#33中提出的)。一个潜在的问题是它使测试更加困难,但并非不可能。例如,您可能必须使用依赖项注入来模拟分叉史诗。

在看到他之前,我已经开始输入答案了,所以我想我也应该把它张贴在后代,以防任何人感兴趣。

我之前也回答了另一个非常相似的问题,这可能会有所帮助:如何将一部史诗延迟到另一部史诗发出一个值之前


我们可以发出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))
    );
Run Code Online (Sandbox Code Playgroud)

这种方法的一个潜在的负面影响是,从理论上讲,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;
Run Code Online (Sandbox Code Playgroud)

然后可以像这样使用它们:

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))
    );
Run Code Online (Sandbox Code Playgroud)

关键细节是初始的“挂起”操作在元对象中拥有唯一的事务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 });
Run Code Online (Sandbox Code Playgroud)

然后,我们可以在整洁的史诗中使用该自定义运算符

const fetchCharacterAndPlanetEpic = (action$, store) =>
  action$.ofType(GET_CHARACTER_AND_PLANET)
    .mergeMap(action =>
      action$.takeFulfilledTransaction(action)
        .mapTo(getPlanet(action.planetId))
        .startWith(getCharacter(action.characterId))
    );
Run Code Online (Sandbox Code Playgroud)

这里描述的事务式解决方案确实实验性的。在实践中,多年来我注意到了一些疣,但我还没有开始思考如何解决它们。就是说,总体而言,这对我的应用程序非常有帮助。实际上,它也可以用于进行乐观更新和回滚!几年前,我将这种模式和可选的乐观更新内容添加到了库redux-transaction中,但是我从未回过头来给它一些爱,因此使用时需自担风险。即使我可能再说一遍,也应将其视为已放弃。