[翻譯] 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 殤

相關文章
相關標籤/搜索