segmentfault.com

拦截器 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个情况吧。

重试最大次数

既然 retryWhen err$ 是一个错误组成的流,那么每一次超时重试失败后, 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, 
    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)
}复制代码

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

分类:
前端
标签: