HttpInterceptor 攔截器 - 網絡請求超時與重試的簡單實現

...

攔截器Angular項目中其實有着十分重要的地位,攔截器可以統一對 HTTP 請求進行攔截處理,我們可以在每個請求體或者響應後對應的流添加一系列動作或者處理數據,再返回給使用者調用。

每個 API 調用的時候都不可避免的會出現網絡超時的情況,但是這種情況是多變的,可能是網絡問題,也有可能是服務端問題,儘管如此,我們也只需對網絡超時這一種情況來進行處理。

套殼

按照慣例寫一個攔截器的殼

import {
    HttpInterceptor,
    HttpRequest,
    HttpHandler,
    HttpEvent
} from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { timeout } from 'rxjs/operators'

/** 攔截器 - 超時以及重試設置 */
@Injectable()
export class TimeoutInterceptor implements HttpInterceptor {

    constructor() { }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req)
    }
}

加入超時處理

超時

rxjs確實功能強大,這裏的超時我們只需要使用timeout操作符便可以實現。這裏的超時處理邏輯是掛到next.handle()返回的可觀察對象中。

next 對象表示攔截器鏈表中的下一個攔截器。 這個鏈表中的最後一個 next 對象就是 HttpClient 的後端處理器(backend handler),它會把請求發給服務器,並接收服務器的響應。
大多數的攔截器都會調用 next.handle(),以便這個請求流能走到下一個攔截器,並最終傳給後端處理器。

先在類外部定義一個超時時限

/** 超時時間 */
const DEFAULTTIMEOUT = 8000

在攔截器主函數handle流中加入操作符

return next.handle(req).pipe(
    timeout(DEFAULTTIMEOUT)
)

其實這樣就實現了超時攔截器,當超過設定的時間還沒有響應數據的時候,handle流便會在拋出相應的超時錯誤。

捕獲超時

在超時錯誤發生後,我們可能需要第一時間捕獲到以便給用戶一個提示。這裏可以直接使用catchError操作符。

在攔截器主函數handle流中加入操作符

return next.handle(req).pipe(
    //... 已有的代碼忽略
    catchError((err: HttpErrorResponse) => {
        this.nzNotificationService.error('網絡超時','請重試')
        return throwError(err)
    })
)

handle需要返回一個可觀察對象,所以我們順便把捕獲的錯誤返回。這樣一來,便可以在捕獲到超時的時候顯示一個簡單的提示。

超時重試

一般來說,超時出現的情況是不確定的,即使多了提示,有些請求用戶也沒有其他的動作去重試,只能刷新頁面,那此時重新請求就顯得重要了,我們可以在捕獲到超時請求之後對這個請求再進行固定次數的重試,避免某些情況的超時影響用戶體驗。

對流進行多次重試,可以使用retryWhen操作符。

retryWhen操作符接受一個函數作爲參數,這個函數會接受一個由一組錯誤組成的Observable,我們可以針對這個Observable做一些節奏控制來促動重試動作,然後在函數中返回這個可觀察對象。

一個簡單的retryWhen組成:

retryWhen(err$ => {
    return err$.pipe(
        //一些節奏控制
        ...
    )
})

如此以來,我們就可以直接使用此操作符來實現了。

添加retryWhen重試

我們在next.handle流掛上retryWhen操作符

return next.handle(req).pipe(
    //... 已有的代碼忽略
    retryWhen(err$ => {
        return err$
    })
)

其實此時就已經實現了重試機制,但是運行結果你會發現,當超時錯誤永遠存在時,重試的次數是無限的,也就是程序會不斷得請求,因爲我們還沒有做任何的節奏控制。

那麼,我們就需要先確定一下重試的節奏,比如最大的重試次數、每次延遲多久重試、重試上限次數還是失敗了的處理等等。那就簡單處理提到的這3個情況吧。

重試最大次數

既然retryWhenerr$是一個錯誤組成的流,那麼每一次超時重試失敗後,err$便會推動一次數據,我們可以使用scan操作符來累計獲取重試失敗的次數,以此來控制重試的最大次數。

scan操作符接受兩個參數,第一個是累加函數,可以在函數中獲取上一次scan的累加值以及所在流的數據,第二個值接受一個scan的初始累加值,所以可以很輕鬆地獲取重試錯誤的次數。

在攔截器類外部定義一個最大重試次數:

/** 最大重試次數 */
const MAXRETRYCOUNT = 3

我們在retryWhen中掛上scan操作符

return next.handle(req).pipe(
    //... 已有的代碼忽略
    retryWhen(err$ => {
        return err$.pipe(
            scan((errCount, err) => {
                if (errCount >= MAXRETRYCOUNT) {
                    throw err
                }
                return errCount + 1
            }, 0)
        )
    })
)

scan中,我們獲取了累加值(errCount,初始爲0 ),判斷是否大於上限,如果大於便直接拋出超時錯誤(err),如果小於便返回累加值 +1。至此,攔截器只會再重試到最大次數還是失敗的情況下拋出超時錯誤。

延遲重試

重試最好加上延遲,避免某些場景下一定請求錯誤的情況,比如服務器的某些請求過濾。延遲十分簡單,只需要在err$掛上delay操作符,流的推動便會以一定的間隔實行。

return next.handle(req).pipe(
    //... 已有的代碼忽略
    retryWhen(err$ => {
        return err$.pipe(
            //... 已有的代碼忽略
            delay(1000)
        )
    })
)
重試的友好提示

可能有的時候網絡太慢,或者重試次數設置得比較大,這樣在請求重試的時候會耗時比較久,而用戶是不知道此時正在重試的,所以加一個友好的提示可以增加用戶體驗。

而添加提示是屬於比較透明或者說屬於副作用動作,此時我們可以直接使用tap操作符來進行操作。由於是掛到scan之後,所以在tap中獲取到的就是重試的累加值。

return next.handle(req).pipe(
    //... 已有的代碼忽略
    retryWhen(err$ => {
        return err$.pipe(
            //... 已有的代碼忽略
            tap(errCount => {
                if(errCount == 1){
                    //第一次重試時顯示友好信息
                    this.nzNotificationService.info('網絡超時','正在重新請求中...')
                }
            })
        )
    })
)

這樣當第一次重新請求時,我們便給出明確的提示。

修改捕獲錯誤(catchError)的順序

前面我們在沒有重試功能之前設置了捕獲錯誤,並給出提示。由於後面加了重試功能,故捕獲錯誤的操作需要掛到重試之後,這樣一來,纔可以在全部重試完成後仍然失敗的情況下提示用戶,而不是每次重試都給出捕獲到的錯誤提示。

return next.handle(req).pipe(
    timeout( ... ),
    retryWhen( ... ),
    catchError( ... )
)

完成上述步驟,一個簡單的網絡請求超時與重試的攔截器便實現了。完整的代碼如下:

import {
    HttpInterceptor,
    HttpRequest,
    HttpHandler,
    HttpEvent,
    HttpErrorResponse
} from '@angular/common/http'
import { Injectable } from '@angular/core'
import { 
    Observable, 
    throwError 
} from 'rxjs'
import { 
    timeout, 
    delay, 
    retryWhen, 
    scan, 
    tap, 
    catchError 
} from 'rxjs/operators'
import { NzNotificationService } from 'ng-zorro-antd'

/** 超時時間 */
const DEFAULTTIMEOUT = 8
/** 最大重試次數 */
const MAXRETRYCOUNT = 3

//攔截器 - 超時以及重試設置
@Injectable()
export class TimeoutInterceptor implements HttpInterceptor {

    constructor(
        private nzNotificationService:NzNotificationService
    ) { }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req).pipe(
            timeout(DEFAULTTIMEOUT),
            retryWhen(err$ => {
                //重試 節奏控制器
                return err$.pipe(
                    scan((errCount, err) => {
                        if (errCount >= MAXRETRYCOUNT) {
                            throw err
                        }
                        return errCount + 1
                    }, 0),
                    delay(1000),
                    tap(errCount => {
                        //副作用
                        if(errCount == 1){
                            //第一次重試時顯示友好信息
                            this.nzNotificationService.info('網絡超時','正在重新請求中...')
                        }
                    })
                )
            }),
            catchError((err: HttpErrorResponse) => {
                this.nzNotificationService.error('網絡超時','請重試')
                return throwError(err)
            })
        )
    }   
}

詳細攔截器說明請前往官網文檔:攔截請求和響應

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章