使用 FetchJsonP / Redux / React 调用 API 时的错误处理

Error Handling on API Call with FetchJsonP / Redux / React

嘿,我正在尝试找出一种方法来处理错误并 api 在 redux 史诗中调用,我已经查看了这个文档: https://redux-observable.js.org/docs/recipes/ErrorHandling.html 我没有任何错误,但没有任何反应,代码似乎在循环

/**
 * Request the JSON guide to the API
 * and then dispatch requestGuideFulfilled & requestGameTask actions
 *
 * @param action$
 */
export function requestGuide(action$) {
  return action$.ofType(REQUEST_GUIDE)
    .mergeMap(({id}) => fetchJsonp(`${API_URL}/guide/${id}/jsonp`)
      .catch(error => requestGuideFailed(error))
    )
    .mergeMap(response => response.json())
    .mergeMap(json => requestGuideFulfilled(json))
    .map(json => requestGameTask(json))
}


export function manageRequestGuideError(action$) {
  return action$.ofType(REQUEST_GUIDE_FAILED)
    .subscribe(({error}) => {
      console.log('Error',error)
    })
}

有什么想法吗?谢谢!

[更新]:即使在获取时我也有错误:

You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.

有很多问题,所以我会尽力详细说明。坦率地说,RxJS不容易。我鼓励您在使用 redux-observable 之前花一些时间学习基础知识,除非您当然只是在空闲时间进行实验以获得乐趣并且喜欢痛苦。

不要引入 redux-observable 之类的东西也很重要,除非你 真的 需要复杂的副作用管理。有点不幸的是,文档目前只有简单的示例,但 redux-observable 的真正目的是使复杂的东西,如多路复用 websockets,详尽的 time-based 排序等,以需要真正了解 RxJS 为代价变得更容易。所以我想我的意思是,如果你确实需要 redux,请确保你也需要 redux-observable 或者可以不用 redux-thunk。 redux-observable 的制造商之一说服人们不要使用它可能看起来很有趣,但我只是看到疯狂的人使用 redux-observable/redux-saga 之类的东西来做一些根本不能证明其复杂性的事情他们带来。 虽然最了解你的需求,所以不要以此为教条或无理气馁<3

None 此答案中的代码已经过测试,因此可能需要稍作更正。


You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.

此错误可能是由 .mergeMap(json => requestGuideFulfilled(json)) 引起的。看起来 requestGuideFulfilled 是一个 action creator,但这里没有包含来源,所以我不能确定。 mergeMap 又名 flatMap 希望你 return 另一个流(通常是一个 Observable),所以一个动作 POJO 需要包装在一个 Observable 中,就像 Observable.of(requestGuideFulfilled(json)) 但在这种情况下使用 mergeMap 是不必要的。它可能只是一个普通的 map().


export function manageRequestGuideError(action$) {
  return action$.ofType(REQUEST_GUIDE_FAILED)
    .subscribe(({error}) => {
      console.log('Error',error)
    })
}

在redux-observable中所有Epics必须return一个Observable。此 Epic 是 return 订阅可观察对象(subscribe() 的 return 值)。这实际上确实会产生错误,但是由于 bug in RxJS it's been silently swallowed.

相反,您可以使用 doignoreElements 创建一个 Observable 来监听该操作,将 属性 记录下来,然后忽略它(永远不会自己发出任何东西)。所以基本上是"read only"。

export function manageRequestGuideError(action$) {
  return action$.ofType(REQUEST_GUIDE_FAILED)
    .do(({error}) => {
      console.log('Error',error)
    })
    .ignoreElements();
}

下一个也是最大的问题是您放置 catch 的位置。重要的是要了解使用 RxJS 意味着我们如何将 Observable 链接在一起——"operators" 基本上采用一个源和 return 另一个 Observable,后者将延迟对通过它们传输的数据应用一些操作。这与使用数组(例如 arr.map().filter())的函数式编程非常相似,但有两个主要区别:Observables 是惰性的并且它们有 time-dimension.

运算符的工作原理

所以考虑这个 Observable 链:

Observable.of(1, 2, 3)
  .map(num => num.toString())
  .filter(str => str !== '2');
  .subscribe(value => console.log(value));
  • 我们创建一个 Observable,当订阅时,它会同步发出 1、2、3。
  • 我们将 map 运算符应用于该源,它创建了一个新的 Observable,当订阅时,它本身将订阅我们应用它的源:我们的 Observable 1、2、3。
  • 然后我们应用 filter 运算符to the Observable returned bymap. As you might have guessed,filterreturns a new Observable that, when subscribed to, will itself subscribe to the source we applied it to: our Observable of strings we mapped. Because thatmap` Observable 本身被应用到一个源,然后它也会订阅它的源, 拉入第一个数字并开始地图 -> 过滤操作。

将这些中间 Observable 存储为变量可能会有所帮助,以揭开神秘面纱。

const source1: Observable<number> = Observable.of(1, 2, 3);
const source2: Observable<string> = source1.map(num => num.toString());
const result: Observable<string> = source2.filter(str => str !== '2');

内部 Observables

当我们使用像 mergeMapswitchMapconcatMap 这样的运算符时,我们是说我们想将每个值映射到另一个 "inner" Observable 的值将是在前一个内部 Observable 之后合并、切换或排队(concat)。它们有不同的重要区别,但如果您不熟悉它们,可以参考很多资源。

在您的例子中,我们使用的是 mergeMap,它还有一个别名 flatMap,这是函数式编程中更广为人知的术语。 mergeMap 将为您的投影函数提供每个值,并同时为您 return 订阅 Observable。每个内部 Observable 的值都合并在一起。

在函数式编程中,他们更常称此为扁平化,而不是合并。因此,首先在 Arrays

的上下文中考虑这个 merging/flattening 可能会再次有所帮助

Array.prototype.map

[1, 3, 5].map(value =>  {
  return [value, value + 1];
});
// [[1, 2], [3, 4], [5, 6]]     Array<Array<number>>

这导致了一个由数字组成的数组 Array<Array<number>>,但是如果我们想要一个单一的扁平化数组呢?输入 flatMap.

Array.prototype.flatMap (stage 2 TC39 proposal)

[1, 3, 5].flatMap(value =>  {
  return [value, value + 1];
});
// [1, 2, 3, 4, 5, 6]           Array<number>

JavaScript 数组还正式有 flatMap,但在撰写本文时它是 stage 2 TC39 proposal。它遵循与典型 flatMap 相同的语义:对于数组中的每个项目,将其映射到投影函数提供的另一个数组,然后将每个项目展平为一个新数组。

对于 Observables,它几乎是一样的,除了它们是惰性的并且有一个时间维度:

Observable.of(1, 3, 5).map(value =>  {
  return Observable.of(value, value + 1);
});
// Observable.of(1, 2)..Observable.of(3, 4)..Observable.of(5, 6)  | Observable<Observable<number>>

我们将每个数字映射到它们自己的两个数字的 Observable 中。所以 higher-order Observable Observable<Observable<number>> 可能不是我们想要的在大多数情况下。

Observable.of(1, 3, 5).flatMap(value =>  {
  return Observable.of(value, value + 1);
});
// 1..2..3..4..5..6                                               | Observable<number>

现在我们只有所有数字的流。完美!

错误处理

结合我们对操作链接和 Observable 扁平化的理解,我们来谈谈错误处理。希望这本入门书能让下一部分更容易理解。

如果我们链式 Observables 中的任何一个抛出错误,它将以与值相同的方式在链中传播,但基本上是在它自己的 "channel" 中。所以如果我们有一个 Observable 链 a -> b -> c 并且在 a 中发生错误,它将被发送到 b 然后 c。当每个 Observable 收到错误时,它可以以某种方式处理它,或者选择将它传递给任何订阅它的人。当它这样做时,该订阅将终止并且不再侦听来自其源的未来消息。

大多数运算符只是传递错误(同时终止),所以如果您没有使用像 catch 这样的特殊错误处理运算符,错误就会传播直到它到达您的观察者——您自己传递给的那个.subscribe(next, error, complete)。如果您提供了该错误处理程序,它将被调用,如果没有,它将作为正常的 JavaScript 异常重新抛出。

为了最终得到你的代码,让我们从头开始;我认为你真正想要的是:

function getGuide(id) {
  const promise = fetchJsonp(`${API_URL}/guide/${id}/jsonp`)
    .then(res => res.json());
  return Observable.from(promise);
}

export function requestGuide(action$) {
  return action$.ofType(REQUEST_GUIDE)
    .mergeMap(({id}) => 
        getGuide(id)
          .mergeMap(json => Observable.of(
            requestGuideFulfilled(json),
            requestGameTask(json)
          ))
          .catch(error => Observable.of(
            requestGuideFailed(error)
          ))
    )
}

现在我们来分解一下。

Promise 与 Observable

您首先会看到我将您的 fetchJsonp 提取到 getGuide 函数中。您也可以将这段代码放在史诗中,但是如果您决定测试,将它分开将使您更容易模拟它。

我尽快将 Promise 包装在 Observable 中。主要是因为如果我们选择使用 RxJS,我们应该选择 all-in,尤其是为了防止以后出现混淆。例如Promise 和 Observable 实例都有 catch 方法,因此如果您开始混合使用两者,很容易导致错误。

理想情况下我们会完全使用 Observables 而不是 Promises,因为 Promises 不能被取消(所以你不能取消实际的 AJAX 请求 + JSON 解析本身),尽管如果你把它包装在一个 Observable 并在 promise 解析之前取消订阅,Observable 将正确地忽略 promise 稍后解析或拒绝的内容。

发出多个动作?

它不是 100% 清楚,但看起来你打算发出两个动作以响应成功取回 JSON。您之前的代码实际上将 JSON 映射到 requestGuideFulfilled() 操作,但是下一个运算符将该操作映射到 requestGameTask()(它不接收 JSON,它接收requestGuideFulfilled() 行动)。记得上面,关于运算符如何成为 Observables 链,值流经它们。

要解决这个问题,我们需要思考"in streams"。我们的 getGuide() Observable 将发出一个值,即 JSON。给定单个 (1) 值,我们希望将其映射到多个其他值,在本例中为两个操作。所以我们要改造one-to-many。我们需要使用 mergeMapswitchMapconcatMap 之一。在这种情况下,由于我们的 getGuide() 永远不会发出超过一项,所以所有这三个运算符都会有相同的结果,但了解它们很重要,因为它通常 很重要 所以记住这一点!在这种情况下,我们只使用 mergeMap

.mergeMap(json => Observable.of(
  requestGuideFulfilled(json),
  requestGameTask(json)
))

Observable.of 支持任意数量的参数,并将按顺序发出每个参数。

捕获错误

因为我们的 Observable 链是......嗯..链 hehe 值在它们之间通过管道传输。正如我们在上面了解到的那样,由于这个 where 你在这些链中放置错误处理很重要。这其实和传统的异常处理,甚至是 promises 的错误处理没有太大区别,但是 Promises 没有 "operators",所以人们通常不会 运行 陷入这种困惑。

catch 运算符是最常见的,它与 catch Promise 非常相似,除了 你必须 return 一个你想要的值的 Observable ,而不是值本身Observable.of 在这里很常见,因为大多数情况下我们只想按顺序发出一个或多个项目。

.catch(error => Observable.of(
  requestGuideFailed(error)
))

每当我们应用此运算符的源发出错误时,它都会捕获它并发出 requestGuideFailed(error) 然后 complete().

因为它发出错误操作,我们应用于此 .catch() ** 的结果的任何运算符也可以对我们的 catch 发出的值进行操作。

getJsonSomehow()
  .catch(error => Observable.of(
    someErrorAction(error)
  ))
  .map(json => {
    // might be the JSON, but also might be the
    // someErrorAction() action!
    return someSuccessAction();
  })

虽然不是 redux-observable 独有的(因为 redux-observable 主要只是一个小型库和一个约定,使用 RxJS)你会经常看到 Epics 遵循类似的模式。

  • 侦听特定操作
  • 然后将该动作合并或切换到执行副作用的内部 Observable 中
  • 当副作用成功时,我们 map 将其作为成功操作
  • 以防出错,我们在 mergeMap/switchMap 中放置一个 catch,但最常见的是在内链的末尾,o 我们发出的任何动作都不会意外改变。

您有望从 redux-observable 文档中识别出一般模式:

function exampleEpic(action$) {
  return action$.ofType(EXAMPLE)
    .mergeMap(action =>
      getExample(action.id)
        .map(resp => exampleSuccess(resp))
        .catch(resp => Observable.of(
          exampleFailure(resp)
        ))
    );
}

将这些知识应用到我们之前的工作中:

getGuide(id)
  .mergeMap(json => Observable.of(
    requestGuideFulfilled(json),
    requestGameTask(json)
  ))
  .catch(error => Observable.of(
    requestGuideFailed(error)
  ))

我想就是这样。


呸!抱歉,那是 long-winded。你完全有可能知道其中的一些或全部,所以如果我在宣讲合唱团,请原谅我!我开始写一些简短的东西,但在澄清之后不断添加澄清。哈哈

如果您正在苦苦挣扎,请务必确保使用 RxJS 和 redux-observable(或任何复杂的中间件)是您的应用程序所必需的复杂性。