[翻译] Angular 最佳实践: RxJS 错误处理

RxJSAngular 的一个重要部分. 若是不了解如何正确地使用 RxJS 处理错误,那么当错误发生时,你确定会遇到一些奇怪的问题.相反,若是你事先知道本身在作什么,你就能够消除这些奇怪的问题,并为本身节省调试的痛苦html

本文将研究git

  • 最须要关注的 RxJS Observables 类型
    • RxJS Infinite Observables 参阅 这篇文章 关于 finite Observablesinfinite Observables的不一样, 尽管你可能猜获得
  • 如何错误地处理 RxJS 中的错误
    • 当错误处理不正确时会发生什么
  • 若是不处理 RxJS 中的错误,会发生什么
  • 如何正确处理 RxJS 中的错误

本文的代码能够在 github 上找到github

Infinite Observables

本文将讨论 infinite observables - 那些你指望能从中一直获取值. 若是你错误的处理错误(do error handling wrong), 它们将再也不是infinite observables, 而且结束 - 这将是很是糟糕的, 由于你的应用程序指望它是infiniteapi

如下这些将会被研究测试

  • DOM Event - 对一个在页面上键入并使用 API 查询的 keyupDOM Event 进行去抖
  • NgRx Effect - 指望始终监听已分派的操做的 NgRx Effect

DOM Event 案例研究

第一个案例研究会聚焦于处理 DOM Event 并基于它们进行搜索. 你将在两个输入框中输入《星球大战》的角色的名字. 当你中止输入 300 毫秒以后, 而且输入的内容和上一次的不相同, 将会使用 星球大战 API 搜索这些名字 而且展现. 第一个输入框会在出现错误以后继续工做,第二个输入框会在出现错误后中止工做.ui

这是界面 this

interface

我稍微修改一下,若是你输入错误,它会搜索一个错误的 URL,从而产生一个错误url

这是相关的 HTMLspa

<input class="form-control" (keyup)="searchTerm$.next($event.target.value)" />

<input class="form-control" (keyup)="searchTermError$.next($event.target.value)" />
复制代码

keyup事件 只是简单的使用 Subjectnext 方法发送数据3d

这是component代码

searchTerm$ = new Subject<string>();
searchTermError$ = new Subject<string>();

this.rxjsService
  .searchBadCatch(this.searchTermError$)
  .pipe(finalize(() => console.log('searchTermError$ (bad catch) finalize called!')))
  .subscribe((results) => {
    console.log('Got results from search (bad catch)');
    this.resultsError = results.results;
  });

this.rxjsService
  .search(this.searchTerm$)
  .pipe(finalize(() => console.log('searchTerm$ finalize called!')))
  .subscribe((results) => {
    console.log('Got results from search (good catch)');
    this.results = results.results;
  });
复制代码

这段代码基本上将向页面发送结果, 并输出日志是否被调用. 注意, 咱们调用了两个不一样的服务方法, 并传入了两个不一样的 Subject

本案例研究的错误处理代码位于 rxjsService 中:

searchBadCatch(terms: Observable<string>) {
  return terms.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(term => this.searchStarWarsNames(term)),
    catchError(error => {
      console.log("Caught search error the wrong way!");
      return of({ results: null });
    })
  );
}

search(terms: Observable<string>) {
  return terms.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(term =>
      this.searchStarWarsNames(term).pipe(
        catchError(error => {
          console.log("Caught search error the right way!");
          return of({ results: null });
        })
      )
    )
  );
}

private searchStarWarsNames(term) {
  let url = `https://swapi.co/api/people/?search=${term}`;
  if (term === "error") {
    url = `https://swapi.co/apix/people/?search=${term}`;
  }

  return this.http.get<any>(url);
}
复制代码

糟糕的错误处理

searchBadCatch 方法实现了糟糕的错误处理. 它看起来没有问题, 对吧? 它在 300 毫秒内去抖动,而且使用distinctUntilChanged确保咱们不会连续两次搜索相同的东西. 在 switchMap 中, 咱们使用了searchStarWarsNames方法,而且使用catchError方法捕获错误. 这有什么问题吗?

若是你在 Observables 的 pipe 方法的第一层使用 catchError 捕捉错误(在本例中是 return terms.pipe()),它将容许你处理错误,而且返回一个或多个结果, 可是它会紧接着终止这个 observable stream(可观察者流). 这意味着它不会再监听 keyup事件. 所以, 不管如何, 毫不容许错误渗透到这一层.

注意, 若是在Observablepipe方法的第一层触达catchError, finalize 方法将会被调用. 你能够在component代码中看到这一点.

这里有个可视化代码(visual), 我但愿有所帮助

visual

永远不要让错误渗透到红线的水平.

良好的错误处理

search 方法中实现了 RxJS 错误处理的最佳实践代码

始终将 catchError 操做符 放在 相似 switchMap 的方法中, 以便于它只结束 API 调用流,而后将流返回 switchMap 中,继续执行Observable. 若是你没有调用 API,确保添加了try/catch 代码来处理错误,而且不容许它渗透到第一层的 pipe, 不要假设你的代码不会失败, 必定要使用 try/catch.

因此, 你能够在代码中看到咱们在 searchStarWarsNames 方法调用中 添加了 pipe 方法,这样咱们就能够捕获错误,从而不容许错误渗透到第一层 pipe

这是最佳处理的可视化代码(visual)

visual

始终在 switchMap / mergeMap / concatMap 等内部的捕获错误

输出(Output)

如今是时候看看它是如何在网页上工做的.咱们假设它一开始是工做的.当 API 调用出错时,就有乐子了.

首先,我将在两个输入框中键入错误,以下所示

我将把它做为练习, 你本身看控制台输出. 如今正式测试,我能够在处理错误后继续输入内容并得到结果吗?

这里咱们看到第一个可行,第二个再也不可行

第二个输入框出现了正如我开头介绍中所说的奇怪的问题.你将很难弄清楚为何你的搜索中止工做

NgRx Effect 案例研究

我开始写这篇文章的缘由是我在一个应用程序使用 NgRx Effects 出现的奇怪的问题. 有关信息请看这里. 多是由于我没有在effect中正确处理 RxJS 错误? 正如你在本研究中所看到的, 答案是确定的

这是界面

这里没有什么特别的

  • Success - 用 person/1(Luke Skywalker)调用星球大战 API,并在屏幕上输出名称
  • Error – Stops Listening - 使用错误 URL 调用 API,所以它会生成一个错误 - catch是错误的,因此它中止监听 effect
  • Error – Don’t catch error - 使用错误的 URL 调用 API,以便生成错误 - 不捕获错误
  • Error – Keeps Listening - 使用错误的 URL 调用 API,以便生成错误 - 正确捕获错误,因此能够屡次单击它

我会跳过 HTML,由于它只是调用组件方法的按钮. 这是组件代码

ngrxSuccess() {
  this.store.dispatch(new CallWithoutError());
}

ngrxError() {
  this.store.dispatch(new CallWithError());
}

ngrxErrorKeepListening() {
  this.store.dispatch(new CallWithErrorKeepListening());
}

ngrxErrorDontCatch() {
  this.store.dispatch(new CallWithErrorNotCaught());
}
复制代码

好(good),坏(bad)和丑陋(ugly)的错误处理都在 effect 代码中

CallWithoutError Effect

这是咱们的成功案例

@Effect()
callWithoutError$ = this.actions$.pipe(
  ofType(AppActionTypes.CallWithoutError),
  switchMap(() => {
    console.log("Calling api without error");

    return this.http.get<any>(`https://swapi.co/api/people/1`).pipe(
      map(results => results.name),
      switchMap(name => of(new SetName(name))),
      catchError(error => of(new SetName("Error!")))
    );
  }),
  finalize(() => console.log("CallWithoutError finalize called!"))
);
复制代码

这个每次都会工做.即便它失败了,它会继续工做,由于 catchErrorhttp.getpipe 中. 在这个成功案例,SetName reducer 将向 store 添加name, 用户界面选择并显示它.

CallWithError Effect

effect将使用错误的 URL 调用 API,所以会生成错误. 错误处理操做不正确,所以一旦调用,这将永远不会再次运行,直到刷新应用程序.

@Effect()
callWithError$ = this.actions$.pipe(
  ofType(AppActionTypes.CallWithError),
  switchMap(() => {
    console.log("Calling api with error - stop listening");

    return this.http.get<any>(`https://swapi.co/apix/people/1`).pipe(
      map(results => results.name),
      switchMap(name => of(new SetName(name)))
    );
  }),
  catchError(error => of(new SetName("Error - You're doomed!"))),
  finalize(() => console.log("CallWithError finalize called!"))
);
复制代码

在这种状况下, catchError 会在 this.actions$.pipe 的第一层中别调用, 从而结束 effect,由于它的 Observable 流将结束. 这就像上面使用 RxJS Observables 的案例研究同样. 点击后咱们应该在页面上看到Error – You’re doomed!. 若是咱们再次尝试单击该按钮,则不会触发该effect.

如下是此输出:

CallWithErrorKeepListening Effect

effect将使用错误的 URL 调用 API,所以会生成错误. 可是,它会正确处理错误,以即可以再次调用它.

@Effect()
callWithErrorKeepListening$ = this.actions$.pipe(
  ofType(AppActionTypes.CallWithErrorKeepListening),
  switchMap(() => {
    console.log("Calling api with error - keep listening");

    return this.http.get<any>(`https://swapi.co/apix/people/1`).pipe(
      map(results => results.name),
      switchMap(name => of(new SetName(name))),
      catchError(error => of(new SetName("Error but still listening!")))
    );
  }),
  finalize(() => console.log("CallWithErrorKeepListening finalize called!"))
);
复制代码

处理 RxJS 错误的正确方法是将 catchError 放在 http.getpipe中. 它将结束 http.getobservable,但这并不重要,由于它不管如何都是finite observable,只发出一个值. 当它返回SetName action时,switchMapemit 并继续 Observable 流. 请注意,此处的finalize将永远不会被调用.

如下是此输出:

CallWithErrorNotCaught Effect

这是咱们的最后一个effect, 并回答了咱们的问题"若是咱们没有 catch 错误会发生什么?" 这个问题的答案是它的行为与咱们不正确地处理错误的行为相同(由于这就是 RxJS 如何运转). 只是你没有处理(hooking into)那个错误流.

@Effect()
callWithErrorDontCatch$ = this.actions$.pipe(
  ofType(AppActionTypes.CallWithErrorNotCaught),
  switchMap(() => {
    console.log("Calling api with error - don't catch");

    return this.http.get<any>(`https://swapi.co/apix/people/1`).pipe(
      map(results => results.name),
      switchMap(name => of(new SetName(name)))
    );
  }),
  finalize(() => console.log("CallWithErrorNotCaught finalize called!"))
);
复制代码

此外,因为你没有 在 catchError 中调用 SetName, 所以不会在 UI 上设置 name. 所以,若是点击第一个按钮,将看不到任何输出,或者将看到上一次设置的 name. 另外一个很难调试的“怪异问题”.

最后

正如你在本文中所知,知道如何在 Angular 应用程序中正确处理的 RxJS 错误将帮助你阻止 infinite Observable 意外结束的奇怪的问题. 利用这些知识,你应该可以确保你的infinite Observables 永远不会结束,直到你决定结束它们为止.

原文


文章如有纰漏请你们补充指正,谢谢~~

blog.xinshangshangxin.com SHANG 殇

相关文章
相关标签/搜索