HttpInterceptor 拦截器 - 网络请求超时与重试的简单实现

...

拦截器Angular项目中其实有着十分重要的地位,拦截器能够统一对 HTTP 请求进行拦截处理,咱们能够在每一个请求体或者响应后对应的流添加一系列动做或者处理数据,再返回给使用者调用。typescript

每一个 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流中加入操做符antd

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

其实这样就实现了超时拦截器,当超过设定的时间尚未响应数据的时候,handle流便会在抛出相应的超时错误。ide

捕获超时

在超时错误发生后,咱们可能须要第一时间捕获到以便给用户一个提示。这里能够直接使用catchError操做符。函数

在拦截器主函数handle流中加入操做符ui

return next.handle(req).pipe(
    //... 已有的代码忽略
    catchError((err: HttpErrorResponse) => {
        this.nzNotificationService.error('网络超时','请重试')
        return throwError(err)
    })
)

handle须要返回一个可观察对象,因此咱们顺便把捕获的错误返回。这样一来,即可以在捕获到超时的时候显示一个简单的提示。this

超时重试

通常来讲,超时出现的状况是不肯定的,即便多了提示,有些请求用户也没有其余的动做去重试,只能刷新页面,那此时从新请求就显得重要了,咱们能够在捕获到超时请求以后对这个请求再进行固定次数的重试,避免某些状况的超时影响用户体验。code

对流进行屡次重试,可使用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)
            })
        )
    }   
}

详细拦截器说明请前往官网文档:拦截请求和响应

相关文章
相关标签/搜索