Angular10教程–7.2-HTTP拦截器

Angular10教程--7.2-HTTP拦截器

们的教程更贴近实战,是我们绕不过的话题。
拦截器可以用一种常规的、标准的方式对每一次 HTTP 的请求/响应任务执行从认证到记日志等很多种隐式任务。 如果没有拦截机制,那么开发人员将不得不对每次 HttpClient 调用显式实现这些任务。比如每次设置请求头中的 token 。

编写拦截器

要实现拦截器,就要实现一个实现了 HttpInterceptor 接口中的 intercept() 方法的类。

生成一个 HeroInterceptor

ng g interceptor http-study/interceptors/hero

默认代码长这样:

// hero.interceptor.ts
import { Injectable } from '@/core';
import {HttpRequest, HttpHandler, HttpEvent, HttpInterceptor} from '@/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class HeroInterceptor implements HttpInterceptor {
  constructor() {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    return next.handle(request);
  }
}

上面代码中的 next 对象表示拦截器链表中的下一个拦截器(在应用中可以设置多个拦截器)。

上面的 intercept() 方法什么改动都没做,只是单纯的将请求转发给下一个拦截器(如果有),并最终返回 HTTP 响应体的 Observable

为了统一设置请求头,我们需要修改请求。但 HttpRequest 和 HttpResponse 实例的属性却是只读(readonly)的,所以修改前,我们需要先把它克隆一份,修改这个克隆体后再把它传给 next.handle()

const copyReq = request.clone();

 

Angular10教程--7.2-HTTP拦截器

// hero.interceptor.ts
...
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  const copyReq = request.clone({
    // 跟上节在服务中设置请求头一致
    headers: request.headers.set('token', 'my-auth-token2').set('self-header', 'test2')
  });
  return next.handle(copyReq);
}

这种在克隆请求的同时设置新请求头的操作太常见了,因此它还有一个快捷方式 setHeaders

const copyReq = request.clone({
  setHeaders: {
    token: 'my-auth-token2',
    'self-header': 'test2'
  }
});

拦截器也是一个由 Angular 依赖注入 (DI)系统管理的服务,也必须先提供这个拦截器类,应用才能使用它。

由于拦截器是 HttpClient 服务的依赖,所以必须在提供 HttpClient 的同一个(或其各级父注入器)注入器中提供这些拦截器。

@NgModule({
  imports: [
    HttpClientModule
    // others...
  ],
  providers: [{ provide: HTTP_INTERCEPTORS, useClass: HeroInterceptor, multi: true }]
})

tips: multi: true 选项会告诉 Angular HTTP_INTERCEPTORS 是一个多重提供者的令牌,表示这个令牌可以注入多个拦截器。

删除我们原来在具体方法中设置的请求头,来验证拦截器是否设置成功。

Angular10教程--7.2-HTTP拦截器

大功告成,我们设置的第一个请求拦截器成功~

同时,我们还可以在响应体中设置统一的错误处理提示:

// hero.interceptor.ts
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  // ...
  return next.handle(copyReq).pipe(
    catchError(err => this.handleError(err))
  );
}
private handleError(error: HttpErrorResponse) {
  if (typeof error.error?.code === 'number') {
    console.error(`服务器端发生错误,状态码:${error.error.code}`);
  } else {
    console.error('请求失败');
  }
  return throwError(error);
}
这样一来,我们的请求服务中的代码就会简练很多。

多个拦截器

可能在应用中会有多个拦截器,这个时候,我们可以将所有的拦截器抽离成一个单独的文件,提供时,引入文件即可:
// interceptors/index.ts
import {HeroInterceptor} from '../http-study/interceptors/hero.interceptor';
import {HTTP_INTERCEPTORS} from '@angular/common/http';

export const httpInterceptorProviders = [
  { provide: HTTP_INTERCEPTORS, useClass: HeroInterceptor, multi: true },
];
依赖注入:
import {httpInterceptorProviders} from './http-study/interceptors';
...
providers: httpInterceptorProviders
Angular 会按照提供它们的顺序应用这些拦截器。 如果提供拦截器的顺序是先 A,再 B,再 C,那么请求阶段的执行顺序就是 A->B->C,而响应阶段的执行顺序则是 C->B->A。

用拦截器记录日志

我们的每个请求都会通过拦截器,基于这个特点,我们可以用它来做日志的记录。
ng g interceptor http-study/interceptors/logging
创建一个服务用来记录日志:
ng g s http-study/services/logger
添加两个方法,分别用来存储日志与打印日志,当然,里面的逻辑你可以根据需求自己定义:
// logger.service.ts
export class LoggerService {
  logs: string[] = [];
  private log(msg: string): void {
    console.log(msg);
  }
  add(msg: string): void {
    this.logs.push(msg);
    this.log(msg);
  }
}
在 logging 拦截器中注入 LoggerService 并实现记录逻辑:
// logging.interceptor.ts
import { Observable } from 'rxjs';
import {LoggerService} from '../services/logger.service';
import {finalize, tap} from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
  constructor(private loggerServe: LoggerService) {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
    const startTime = Date.now(); // 记录请求开始时间
    let status: string; // 记录请求状态

    return next.handle(request)
      .pipe(
        tap(
          event => status = event instanceof HttpResponse ? 'succeeded' : '',
          error => status = 'failed'
        ),
        finalize(() => {
          const elapsedTime = Date.now() - startTime; // 请求最后结束时间
          const message = `${request.method} "${request.urlWithParams}" ${status} in ${elapsedTime} ms.`;
          this.loggerServe.add(message);
        })
      );
  }
}
RxJS 的 tap 操作符会捕获请求成功了还是失败了, finalize 操作符无论在响应成功还是失败时都会调用,然后把结果汇报给 LoggerService。 这个拦截器不会对请求与相应做任何修改,原样发送给调用者。(最后别忘了提供这个拦截器)最后我们的效果应该是这样的:Angular10教程--7.2-HTTP拦截器

拦截器实现缓存

拦截器还可以自行处理请求,而不用转发给 next.handle()。利用这一点特性,我们可以做缓存请求和响应,以便提升性能。创建一个 caching 拦截器:
ng g interceptor http-study/interceptors/caching

在实现拦截器逻辑之前,我们先来定义一个 Caching 接口:

// caching.ts
export interface Caching {
  get(req: HttpRequest<any>): HttpResponse<any> | nill;
  put(req: HttpRequest<any>, res: HttpResponse<any>): void;
}

get() 方法用来获取 req 请求对应的响应对象; put() 方法用于保存 req 请求对应的响应对象。

另外,我们还需要定义一个 CachingEntry 接口用来保存已缓存的响应对象。 同时定义一个常量用来设定缓存的有效期时间:

// caching.ts
export interface CachingEntry {
  url: string; // 被缓存的请求地址
  response: HttpResponse<any>; // 被缓存的响应对象
  entryTime: number; // 响应对象被缓存的时间,用于判断缓存是否过期
}
// 设置最长缓存时间为3s
export const MAX_CACHE_AGE = 3000;

现在,我们来实现一个 CachingService 服务,用来实现我们 Caching 接口:

ng g s http-study/services/caching
// caching.service.ts
import { Injectable } from '@angular/core';
import {Caching, CachingEntry, MAX_CACHE_AGE} from '../interfaces/caching';
import {HttpRequest, HttpResponse} from '@angular/common/http';
import {LoggerService} from './logger.service';

@Injectable()
export class CachingService implements Caching{
  // 定义一个map,用于保存缓存,key值是url
  cacheMap = new Map<string, CachingEntry>();
  // 注入开始的日志拦截器
  constructor(private loggerServe: LoggerService ) { }
  // 获取已缓存的响应体
  get(req: HttpRequest<any>): HttpResponse<any> | null {
    // 试图获取缓存
    const entry = this.cacheMap.get(req.urlWithParams);
    // 判断是否被缓存,如没有直接返回null
    if (!entry) { return null; }
    // 判断是否过期
    const isExpired = Date.now() - entry.entryTime > MAX_CACHE_AGE;
    // 添加日志
    this.loggerServe.add(`${req.urlWithParams} is Expired: ${isExpired}`);
    // 有缓存且未过期,返回响应体
    return isExpired ? null : entry.response;
  }
  // 添加缓存
  put(req: HttpRequest<any>, res: HttpResponse<any>): void {
    // 创建CachingEntry对象
    const entry: CachingEntry = {
      url: req.urlWithParams,
      response: res,
      entryTime: Date.now()
    };
    // 添加保存日志
    this.loggerServe.add(`Save ${entry.url} response into cache`);
    // 保存缓存
    this.cacheMap.set(entry.url, entry);
    // 删除过期缓存
    this.deleteExpiredCache();
  }
  private deleteExpiredCache(): void {
    this.cacheMap.forEach((entry: CachingEntry) => {
      if (Date.now() - entry.entryTime > MAX_CACHE_AGE) {
        this.cacheMap.delete(entry.url);
      }
    });
  }
}

万事俱备,只差实现 CachingInterceptor 了。

// caching.interceptor.ts
@Injectable()
export class CachingInterceptor implements HttpInterceptor {
  // 首先注入CachingService服务
  constructor(private cachingServe: CachingService) {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // 判定是否需要缓存,如不需要缓存,直接转给下一个拦截器
    if (!this.isCacheable(request)) {
      return next.handle(request);
    }
    // 获取缓存
    const cachingRes: HttpResponse<any> = this.cachingServe.get(request);
    // 如果有缓存,则直接返回一个缓存的Observable, 否则就需要发送请求,并将响应结果缓存
    return cachingRes ? of(cachingRes) : this.sendRequest(request, next);
  }

  private sendRequest(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          this.cachingServe.put(req, event); // 更新缓存
        }
      })
    );
  }
  private isCacheable(req: HttpRequest<any>): boolean {
    // 这里简单判定了一下,可以根据自己需求去实际判断
    return req.method === 'GET';
  }
}
到此为止,我们就实现了 CachingInterceptor 全部功能,记得提供这个拦截器。

Angular10教程--7.2-HTTP拦截器

从上面的效果可以看出,当我们第一次请求的时候缓存了相应结果,第二次是直接获取的缓存,当与第一请求间隔3s后,再次请求时,缓存已过期,所以就再次发送了请求。同时,从浏览器的 Network 中也能看到只发送了真正的两次请求。

用拦截器来请求多个值

HttpClient.get() 方法通常会返回一个可观察对象,拦截器可以把它改成一个可以发出多个值的可观察对象。

我们在拦截器中加一段判定:

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  // ...
  // 先获得缓存
  if (request.headers.get('x-refresh')) {
    const results$ = this.sendRequest(request, next);
    return cachingRes ?
      results$.pipe( startWith(cachingRes) ) :
      results$;
  }
  // 缓存或者直接请求
  return cachingRes ? of(cachingRes) : this.sendRequest(request, next);
}
这样一来,如果没有缓存值,拦截器直接返回 results$,否则先将缓存的响应加入到 result$ 的管道中,使用重组后的可观察对象进行处理,并发出两次。先立即发出一次缓存的响应体,然后发出来自服务器的响应。

总结

  1. cli 工具生成拦截器是没有简写方式的,应该是 ng g interceptor xxx ;
  2. 拦截器请求顺序是按照配置顺序执行,响应拦截则是相反的顺序;
  3. 用拦截器做请求缓存能优化性能,是个好东西;
  4. 必须在提供 HttpClient 的同一个(或其各级父注入器)注入器中提供拦截器。