Angular HttpClient拦截器在开发中的实践

Angular HttpClient拦截器在开发中的实践

前端组件库 @ 华为

DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站:devui.design
Ng组件库:ng-devui(欢迎Star)

引言

Angular自带的http拦截器,官方介绍:拦截请求和响应,其实描述的已经非常明确,在我们团队实际的业务开发中,主要的实践也是通过自行封装拦截器,来实现对http请求和响应的处理、监视,这两个链路可做的处理场景有很多,比如request header的字段修改,请求的安全鉴权,接口数据缓存,请求的拦截、监视处理等,本文主要从三个实践分享下:1、请求头的设置,2、接口数据缓存,3、响应的处理。代码的实现其实很简单,废话不多说,直接看代码。angular版本7.0,6以上的版本应该都支持。

推荐阅读angular中文官方文档拦截器小节:angular.cn/guide/http#…

开始

我们先实现一个空白拦截器,首先我们需要先实现HttpInterceptor的接口,然后实现里面intercept()方法。代码是这样式的:

import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';

export class ApiInjector implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req);
  }
}
复制代码

intercept要求我们返回一个Observable对象供组件订阅使用,该方法有两个形参:

1、req:HttpRequest类型,这个是我们http请求对象;

2、next:HttpHandler类型,HttpHandler是一个抽象类,它提供了handle()方法,可以把HTTP请求转换成HttpEvent类型的Observable,最终处理的的是来自服务器的响应。这样子在这个方法内部,我们就拿到了request和response,基本可以为所欲为了。

声明之后我们需要在app的根模块注入这个拦截器

@NgModule({
  imports: [
    BrowserModule,
    HttpClientModule,
    BrowserAnimationsModule,
  ],
  declarations: [
    AppComponent,
  ],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: ApiInjector, multi: true }
  ],
  bootstrap: [AppComponent]
})

export class AppModule { }
复制代码

直接使用这种方式,我们就可以使用这个拦截器,但是直接注入进来,随着声明的拦截器越来越多可维护性会随之降低,建议我们增加一个index.ts文件把所有拦截器封装成一个数组对象然后暴露出来,可以在项目里面新开一个文件夹,然后把自定义的拦截器和集成的ts文件放在了一个文件夹里面。index.ts的代码如下,比较简单

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { ApiInjector } from './apiinjector';

// 拦截器数组
export const HttpInterceptorProviders = [
  { provide: HTTP_INTERCEPTORS, useClass: ApiInjector, multi: true }
]
复制代码

把拦截器封装之后,在app.module.ts的providers里面注入HttpInterceptorProviders即可,仔细想下也是合理,后续我们再增加拦截器,直接在数组里面不断追加就可以了,方便维护。

那么问题来了,拦截器多了之后顺序是咋样的?

它的执行顺序其实就是export的这个数组的索引下标顺序0->1->2,写到这里此时我们在项目里面重新调用接口的时候就可以在拦截器里面把intercept方法的俩形参打印出来。

场景1:拦截请求,修改请求参数

一个比较常见的业务场景就是我们在请求头里面增加cftk、cookie等用户鉴权的参数,以cookie为例。

在实操中,我们发现HttpRequest对象没有set**相关的方法,唯一的headers也只是一个只读的属性值,不过当我们查看这个对象的API时发现,里面有一个clone()方法,支持传入自定义的对象数据,在这里我们修改我们的空白拦截器用来处理请求的参数设置功能;

修改之前我们写好的空白的ApiInjector类的intercept方法,代码是这样式的:

intercept(req: HttpRequest<any>, next: HttpHandler) {
  const resetReq = req.clone({ setHeaders: {'Appraisal_Cookie', 'Amazing_039'}});
  console.log(resetReq, 'resetReq');
  return next.handle(resetReq);
}
复制代码

然后在chrome浏览器的F12里面查看我们的http请求:

场景2:捕获响应,缓存数据

在单页应用中,基本都会遇到一个绕不过去的点,就是部分数据的全局共享,在我们团队内部之前比较常用的实现方式是使用rxjs一处广播,处处订阅的来实现数据全局共享。

一般是在app.module.ts注入一个全局的单例service,简单描述下:

public cacheDate = new BehaviorSubject<any>(null);
复制代码

// 在我们拿到数据之后,

this.cacheDate.next('sharedData');
复制代码

// 然后在下层的子组件,通过订阅获取,这个全局缓存的值

cacheDate.asObservable().subscribe(res => {console.log(res)});
复制代码

现在我们看一下借助于拦截器如何实现接口数据的缓存,我们重新增加了一个CacheInjector用作数据缓存的拦截器,根据下面的实现方式,我们可以通过维护cachebleUrlList数组对象来维护我们需要缓存的接口数据,代码是这样式的:

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http';
import { filter, tap } from 'rxjs/operators';
import { of } from 'rxjs';

export class CacheInjector implements HttpInterceptor {
  private cachebleUrlList = ['/deliverboard/v1/userBoard/userInfo'];
  private cacheRequetMap = new Map();
  intercept(req: HttpRequest<any>, next: HttpHandler) {

    // 判断是否需要缓存此条请求
    if (!this.canCacherReq(req)) {
      return next.handle(req);
    }

    // 判断目标缓存请求的值是否初始化
    const cachedResponse = this.cacheRequetMap.get(req.url);
    if (cachedResponse) {
      return of(cachedResponse);
    }

    // 如果没有初始化,在获取到response之后缓存到map里面
    return next.handle(req).pipe(
      filter(event => event instanceof HttpResponse),
      tap(event => {
        console.log(event, '响应事件');
        this.cacheRequetMap.set(req.url, event);
      })
    )
  }

  // 判断当前请求是否需要缓存
  canCacherReq(req: HttpRequest<any>): Boolean {
    return this.cachebleUrlList.indexOf(req.url) !== -1;
  }
}
复制代码

然后在我们的index.ts文件里面,增加我们新加的拦截器的引用,此时我们的缓存拦截器就生效了。

export const HttpInterceptorProviders = [
  { provide: HTTP_INTERCEPTORS, useClass: ApiInjector, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: CacheInjector, multi: true },
];
复制代码

上面两种方式都可以实现数据缓存,大家可以根据实际的业务场景按需所取,此外还要考虑接口的并发问题和大数据量问题。

基于CacheInjector的代码,我们衍生可以个稍微完善点的拦截器,就是拦截器额外暴露两个静态方法提供给业务层,查询已经缓存的url列表,以及清除制定的缓存接口,解决我们在实际开发中,可能有的数据缓存之后会发生变化,此时我们需要重新获取一个新的http请求;代码是这样是的:

import { HttpInterceptor, HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http';
import { filter, tap } from 'rxjs/operators';
import { of } from 'rxjs';

export class CacheInjector implements HttpInterceptor {
  static cachebleUrlList = ['/deliverboard/v1/userBoard/userInfo'];
  static cacheRequetMap = new Map();
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    // 判断是否需要缓存此条请求
    if (!this.canCacherReq(req)) {
      return next.handle(req);
    }

    // 判断目标缓存请求的值是否初始化
    const cachedResponse = CacheInjector.cacheRequetMap.get(req.url);
    if (cachedResponse) {
      return of(cachedResponse);
    }

    // 如果没有初始化,在获取到response之后缓存到map里面
    return next.handle(req).pipe(
      filter(event => event instanceof HttpResponse),
      tap(event => {
        console.log(event, '响应事件');
        CacheInjector.cacheRequetMap.set(req.url, event);
      })
    );
  }

  // 判断当前请求是否需要缓存
  canCacherReq(req: HttpRequest<any>): boolean {
    return CacheInjector.cachebleUrlList.indexOf(req.url) !== -1;
  }

  // 查询缓存的接口列表
  static getCachedUrlList(): string[] {
    return [...CacheInjector.cacheRequetMap.keys()];
  }

  // 外部主动刷新
  static refreshReq(req) {
    CacheInjector.cacheRequetMap.delete(req);
  }
}
复制代码

场景3:捕获响应错误,集中处理

在web项目里面,用户的一些特殊操作场景,或者开发小兄弟用心准备的bug小惊喜,会导致我们的请求如期报错了,此时我们可以写一个捕获错误的拦截器,在拦截器里面对请求发生的错误进行统一处理,废话不多说,在统一存在拦截器文件夹目录下面,新增一个文件CatchErrorInjector,代码是这样式的:

import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';
import { catchError, retry } from 'rxjs/operators';

export class CatchErrorInjector implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    return next.handle(req).pipe(
      catchError(err => {
        // 统一处理错误信息,可以使用项目已有的消息组件抛出错误信息,也可以根据请求的错误码类型做更多的处理,
        console.log(err, '后端接口报错');
        throw err;
      }),
    );
  }
}
复制代码

这样处理之后,后续也可以帮助我们很方便的对报错的字段进行国际化的处理。但是了,但是了,有的时候我们的请求就是会莫名奇妙的报错,尤其是在网络不稳定的情况下,只要调整姿势再来一次,就会得到一个正常的结果。

此时我们可以借助于retry,更新intercept方法如下,增加retry机制;

intercept(req: HttpRequest<any>, next: HttpHandler) {
  return next.handle(req).pipe(
    catchError(err => {
      // 统一处理错误信息,可以使用项目已有的消息组件抛出错误信息,也可以根据请求的错误码类型做更多的处理,
      console.log(err, '后端接口报错');
      throw err;
    }),
  );
  retry(1)
}
复制代码

retry会在接口报错后立即重新执行一次;仔细一想是不是有点粗暴了,比如我们期望请求400/404的时候重发就可以了,可以使用retryWhen的操作符,这里我是用直接用的catchError的另外形参,在这里我们的实际场景是,既要满足对指定status的接口重发,又要在报错的情况下把我们的消息给用户知会出来,ok,最终代码是这样式的:

import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';
import { catchError } from 'rxjs/operators';

const MAX_RETRY_NUM = 2;

export class CatchErrorInjector implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    let count = 0;
    return next.handle(req).pipe(
      catchError((err, err$) => {
        count++;
        // err$其代表上游的Observable对象,当直接返回这个对象时,会启动catchError的重试机制。
        const tip = err.status === 200 ? err.body.error.reason : '系统繁忙,请稍后再试';
        console.log(tip, '后端接口报错');
        if (err.status === 400 && count < MAX_RETRY_NUM) {
          console.log(count, '重试次数');
          return err$;
        } else {
          throw err;
        }
      }),
    );
  }
}
复制代码

总结

ok,上面就是在业务开发中对于httpclient拦截器的三点小探索:

1、请求头参数更新;

2、接口数据缓存;

3、HTTP响应的错误集中处理。

我这也是主要是抛砖引玉,大家也可以看一下我上面发的中文官方文档里面的关于拦截器的介绍,大同小异。

一个需求点的上线一般都有多种实现方式,所谓道路千万条,速度第一条,手里的武器多一点,代码梭哈自然快。

最后,业务开发本来就朴实无华且枯燥的,没事看看文档,更新下实现方法,重构下业务组件,给平静的交付整点波澜,多好。

加入我们

我们是DevUI团队,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com

文/DevUI ixiaoxiaomi

往期文章推荐

《【译】Angular最佳实践》

《微前端在企业级应用中的实践(上)》

《手把手教你搭建一个灰度发布环境》

文章分类
前端
文章标签