为什么使用 catchError 而不是在 Angular 中的订阅错误回调中处理错误

Why handle errors with catchError and not in the subscribe error callback in Angular

所以我通常会这样写我的 http 请求

服务

getData() {
  return this.http.get('url')
}

组件

getTheData() {
  this.service.getData().subscribe(
    (res) => {
      //Do something
    }, 
    (err) => {
      console.log('getData has thrown and error of', err)
    })

但是查看 Angular 文档,他们似乎在服务中将其格式化为这样

getHeroes(): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl)
    .pipe(
      catchError(this.handleError('getHeroes', []))
    );
}

这有什么隐含的好处,因为它对我来说似乎很冗长,而且我个人从来没有需要表达我的错误。

根据Angular团队
"handleError() method reports the error and then returns an innocuous result so that the application keeps working"

因为每个服务方法 return 都是不同类型的 Observable 结果,像 handleError() 这样的 catchError 函数接受一个类型参数,所以它可以 return 安全值作为应用程序的类型期待。

1 Angular

中的关注点分离

使用 catchError 的一个主要好处是 将整个数据检索逻辑(包括在数据呈现过程中可能发生的所有错误)分开。

1.1 让组件只关心数据的呈现

组件应该只关心数据(无论它存在与否)。他们不应该关心如何检索数据或数据检索期间可能出错的所有细节。

Components shouldn't fetch or save data directly and they certainly shouldn't knowingly present fake data. They should focus on presenting data and delegate data access to a service.
[Angular Tutorial - Why Services]

假设您的数据是一个项目列表。您的组件将调用 service.getItemList() 函数,并且由于它只关心数据,因此期望:

  • 包含项目的列表
  • 空列表
  • 没有列表即 nullundefined

您可以在组件模板中使用 ngIf 轻松处理所有这些情况,并根据情况显示数据或其他内容。有一个服务函数 return 一个 clean Observable 只有 returns 数据(或 null)并且预计不会抛出任何错误保持组件中的代码精简,因为您可以轻松地使用模板中的 AsyncPipe 进行订阅。

1.2 不要让组件关心错误等数据检索细节

您的数据检索和错误处理逻辑可能会随着时间而改变。也许您正在升级到新的 Api 并且突然不得不处理不同的错误。不要让你的组件担心这个。将此逻辑移至服务。

Removing data access from components means you can change your mind about the implementation anytime, without touching any components. They don't know how the service works. [Angular Tutorial - Get hero data]

1.3 将数据检索和错误处理逻辑放在一个服务中

处理错误是数据检索逻辑的一部分,而不是数据表示逻辑的一部分。

在您的数据检索服务中,您可以使用 catchError 运算符详细处理错误。也许您想对所有错误执行一些操作,例如:

  • 登录
  • 将面向用户的错误消息显示为通知(请参阅 Show messages
  • 获取替代数据或return默认值

将其中的一些移到 this.handleError('getHeroes', []) 函数中可以避免重复代码。

After reporting the error to console, the handler constructs a user friendly message and returns a safe value to the app so it can keep working. [Angular Tutorial - HTTP Error handling]

1.4 让以后的开发更简单

有时您可能需要从新组件调用现有服务功能。在服务函数中包含错误处理逻辑使这变得很容易,因为从新组件调用该函数时您不必担心错误处理。

因此归结为将数据检索逻辑(在服务中)与数据表示逻辑(在组件​​中)分开,以及将来扩展应用程序的难易程度。

2 保持 Observable 活动

catchError 的另一个用例是在构建更复杂的链式或组合 Observable 时保持 Observable 活动。在内部 Observable 上使用 catchError 允许您从错误中恢复并保持外部 Observable 运行。当您使用订阅错误处理程序时,这是不可能的。

2.1 链接多个 Observables

看看这个longLivedObservable$:

// will never terminate / error
const longLivedObservable$ = fromEvent(button, 'click').pipe(
  switchMap(event => this.getHeroes())
);
longLivedObservable$.subscribe(console.log);

getHeroes(): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl).pipe(
    catchError(error => of([]))
  );
}

longLivedObservable$ 将在单击按钮时执行 http 请求。它永远不会终止,即使内部 http 请求抛出错误,在这种情况下 catchError returns 是一个没有错误但发出空数组的 Observable。

如果您要向 longLivedObservable$.subscribe() 添加错误回调并在 getHeroes 中删除 catchError,则 longLivedObservable$ 将在第一个抛出错误的 http 请求后终止,并且之后再也不会对按钮点击做出反应。


附录:添加到哪个 Observable 很重要 catchError

请注意,如果您将 catchErrorgetHeroes 中的内部 Observable 移动到外部 Observable,longLivedObservable$ 将终止

// will terminate when getHeroes errors
const longLivedObservable = fromEvent(button, 'click').pipe(
  switchMap(event => this.getHeroes()),
  catchError(error => of([]))
);
longLivedObservable.subscribe(console.log); 

getHeroes(): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl);
}

"Error" and "Complete" notifications may happen only once during the Observable Execution, and there can only be either one of them.

In an Observable Execution, zero to infinite Next notifications may be delivered. If either an Error or Complete notification is delivered, then nothing else can be delivered afterwards.
[RxJS Documentation - Observable]

Observables 在发送错误(或完成)通知时终止。他们之后不能发出任何其他东西。在 Observable 上使用 catchError 不会改变这一点。 catchError 不允许您的源 Observable 在发生错误后继续发射,它只允许您在发生错误时切换到不同的 Observable。此切换只会发生一次,因为只能传送一个错误通知。

在上面的示例中,当 this.getHeroes() 出错时,此错误通知会传播到外部流,导致从 fromEvent(button, 'click') 取消订阅,并且 catchError 切换到 of([])

在内部 Observable 上放置 catchError 不会将错误通知暴露给外部流。因此,如果你想让外部 Observable 保持活动状态,你必须在内部 Observable 上使用 catchError 处理错误,即直接在错误发生的地方处理。


2.2 合并多个 Observables

当您组合 Observables 时,例如使用 forkJoincombineLatest 你可能希望外部 Observable 在任何内部 Observable 错误时继续。

const animals$ = forkJoin(
  this.getMonkeys(), 
  this.getGiraffes(), 
  this.getElefants()
);
animals$.subscribe(console.log);

getMonkeys(): Observable<Monkey[]> {
  return this.http.get<Monkey[]>(this.monkeyUrl).pipe(catchError(error => of(null)));
}

getGiraffes(): Observable<Giraffe[]> {
  return this.http.get<Giraffe[]>(this.giraffeUrl).pipe(catchError(error => of(null)));
}

getElefants(): Observable<Elefant[]> {
  return this.http.get<Elefant[]>(this.elefantUrl).pipe(catchError(error => of(null)));
}

animals$ 将发出一个数组,其中包含它可以获取的动物数组,或者 null 获取动物失败的数组。例如

[ [ Gorilla, Chimpanzee, Bonobo ], null, [ Asian Elefant, African Elefant ] ]

这里 catchError 允许 animals$ Observable 完成并发出一些东西。

如果您要从所有获取函数中删除 catchError 并向 animals$.subscribe() 添加错误回调,那么 animals$ 会在任何内部 Observables 错误时出错,因此不会发出任何内容即使一些内部 Observables 成功完成。

要了解更多信息,请阅读:RxJs Error Handling: Complete Practical Guide

刚发现这个问题,我想我会更新我的发现以更好地回答我自己的问题。

虽然从组件中抽象出错误处理逻辑的要点是一个完全有效的要点,也是主要的要点之一,但还有其他几个原因导致使用 catchError 而不是仅使用订阅错误方法处理错误.

主要原因是 catchError 允许您处理从 http.get 或管道方法中出错的第一个运算符返回的可观察对象,即:

this.http.get('url').pipe(
  filter(condition => condition = true), 
  map(filteredCondition => filteredCondition = true), 
  catchError(err => {
    return throwError(err);
  })
).subscribe(() => {});

因此,如果这些运算符中的任何一个失败,无论出于何种原因,catchError 将捕获从中返回的可观察到的错误,但我使用 catchError 遇到的主要好处是它可以防止可观察流在发生错误时关闭。

使用 throwErrorcatchError(err throw 'error occcured') 将导致调用订阅方法的错误部分,从而关闭可观察流,但是使用 catchError 如下:

示例一:

// Will return the observable declared by of thus emit the need to trigger the error on the subscription
catchError(err, of({key:'streamWontError'}));

例子二:

// This will actually try to call the failed observable again in the event of the error thus again preventing the error method being invoked. 
catchError(err, catchedObservable});