前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Angular HttpClient 拦截器

Angular HttpClient 拦截器

作者头像
阿宝哥
发布2019-11-05 15:52:06
2.5K0
发布2019-11-05 15:52:06
举报
文章被收录于专栏:全栈修仙之路全栈修仙之路

在之前的 Angular 6 HttpClient 快速入门 文章中,我们已经简单介绍了 Http 拦截器。本文将会进一步分析一下 Http 拦截器。拦截器提供了一种用于拦截、修改请求和响应的机制。这个概念与 Node.js 的 Express 框架中间件的概念类似。拦截器提供的这种特性,对于日志、缓存、请求授权来说非常有用。

AuthInterceptor

auth.interceptor.ts

import { Injectable } from "@angular/core";
import { HttpEvent, HttpRequest, HttpHandler, HttpInterceptor } from "@angular/common/http";

import { Observable } from "rxjs";

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const clonedRequest = req.clone({
      headers: req.headers.set("X-CustomAuthHeader", "iloveangular")
    });
    console.log("new headers", clonedRequest.headers.keys());
    return next.handle(clonedRequest);
  }
}

要实现自定义拦截器,首先我需要定义一个类并实现 HttpInterceptor 接口:

export interface HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>;
}

实现 HttpInterceptor 接口,就需要实现该接口中定义的 intercept(),该方法接收两个参数:

  • req:HttpRequest 对象,即请求对象。
  • next:HttpHandler 对象,该对象有一个 handle() 方法,该方法返回一个 Observable 对象。

在上面的 AuthInterceptor 拦截器中,我们实现的功能就是设置自定义请求头。接下来我们来介绍如何利用拦截器实现请求日志记录的功能。

LoggingInterceptor

下面我们来定义 LoggingInterceptor 拦截器,该拦截器实现的功能是记录每个请求的响应状态和时间。

logging.interceptor.ts

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http';
import { finalize, tap } from 'rxjs/operators';
import { LoggerService } from '../logger.service';

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {

  constructor(private loggerService: LoggerService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const startTime = Date.now();
    let status: string;

    return next.handle(req).pipe(
        tap(
          event => {
            status = '';
            if (event instanceof HttpResponse) {
              status = 'succeeded';
            }
          },
          error => status = 'failed'
        ),
        finalize(() => {
          const elapsedTime = Date.now() - startTime;
          const message = req.method + " " + req.urlWithParams +" "+ status 
          + " in " + elapsedTime + "ms";
          
          this.loggerService.log(message);
        })
    );
  }
}

logger.service.ts

import { Injectable } from "@angular/core";

@Injectable({
  providedIn: "root"
})
export class LoggerService {
  log(msg: string): void {
    console.log(msg);
  }

  error(msg: string, obj = {}): void {
    console.error(msg, obj);
  }
}

定义完 LoggingInterceptor 拦截器,在使用它之前还需对它进行配置:

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

接着我们来继续更新一下 AppComponent 根组件:

import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";

@Component({
  selector: "app-root",
  template: `
    <h3>Angular Http Interceptor</h3>
    <button (click)="getUsers()">Get Users</button>
 `,
  styles: [`button {border: 1px solid blue;}`]
})
export class AppComponent {
  constructor(public http: HttpClient) {}

  getUsers(): void {
    this.http
      .get("http://jsonplaceholder.typicode.com/users")
      .subscribe(res => {
        console.dir(res);
      });
  }
}

然后启动应用,当我们点击 Get Users 按钮时,控制台会输出一下信息:

GET http://jsonplaceholder.typicode.com/users succeeded in 728ms

好的,趁热打铁,我们再来一个例子,即介绍如何利用拦截器实现简单的缓存控制。

CachingInterceptor

在实现缓存拦截器之前,我们先来定义一个 Cache 接口:

import { HttpRequest, HttpResponse } from '@angular/common/http';

export interface Cache {
  get(req: HttpRequest<any>): HttpResponse<any> | null;
  put(req: HttpRequest<any>, res: HttpResponse<any>): void;
}

上面定义的 Cache 接口中,包含两个方法:

  • get(req: HttpRequest): HttpResponse| null —— 用于获取 req 请求对象对应的响应对象;
  • put(req: HttpRequest, res: HttpResponse): void; —— 用于保存 req 对象对应的响应对象。

另外在实际的场景中,我们一般都会为缓存设置一个最大的缓存时间,即缓存的有效期。在有效期内,如果缓存命中,则会直接返回已缓存的响应对象。下面我们再来定义一个 CacheEntry 接口,该接口包含三个属性:

  • url: string —— 被缓存的请求 URL 地址
  • response: HttpResponse—— 被缓存的响应对象
  • entryTime: number —— 响应对象被缓存的时间,用于判断缓存是否过期

此外,我们还要定义一个常量,用于设定缓存的有效期,这里我们假设缓存的时间为 30 s,具体如下:

import { HttpResponse } from "@angular/common/http";

export const MAX_CACHE_AGE = 30000; // 单位为毫秒

export interface CacheEntry {
  url: string;
  response: HttpResponse<any>;
  entryTime: number;
}

定义完 Cache 和 CacheEntry 接口,我们来实现 CacheService 服务:

import { Injectable } from "@angular/core";
import { HttpRequest, HttpResponse } from "@angular/common/http";

import { Cache } from "./cache";
import { CacheEntry, MAX_CACHE_AGE } from "./cache.entry";
import { LoggerService } from './logger.service';

@Injectable({
  providedIn: "root"
})
export class CacheService implements Cache {
  cacheMap = new Map<string, CacheEntry>();

  constructor(private logger: LoggerService) {}

  get(req: HttpRequest<any>): HttpResponse<any> | null {
    // 判断当前请求是否已被缓存,若未缓存则返回null
    const entry = this.cacheMap.get(req.urlWithParams);
    if (!entry) return null;
    // 若缓存命中,则判断缓存是否过期,若已过期则返回null。否则返回请求对应的响应对象  
    const isExpired = Date.now() - entry.entryTime > MAX_CACHE_AGE;
    this.logger.log(`req.urlWithParams is Expired: ${isExpired} `);
    return isExpired ? null : entry.response;
  }

  put(req: HttpRequest<any>, res: HttpResponse<any>): void {
    // 创建CacheEntry对象  
    const entry: CacheEntry = {
      url: req.urlWithParams,
      response: res,
      entryTime: Date.now()
    };
    this.logger.log(`Save entry.url response into cache`);
    // 以请求url作为键,CacheEntry对象为值,保存到cacheMap中。并执行
    // 清理操作,即清理已过期的缓存。
    this.cacheMap.set(req.urlWithParams, entry);
    this.deleteExpiredCache();
  }

  private deleteExpiredCache() {
    this.cacheMap.forEach(entry => {
      if (Date.now() - entry.entryTime > MAX_CACHE_AGE) {
        this.cacheMap.delete(entry.url);
      }
    });
  }
}

现在万事俱备只欠 “东风导弹” —— CachingInterceptor:

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

import { CacheService } from '../cache.service';

const CACHABLE_URL = "http://jsonplaceholder.typicode.com";

@Injectable()
export class CachingInterceptor implements HttpInterceptor {
    constructor(private cache: CacheService) {}

    intercept(req: HttpRequest<any>, next: HttpHandler) {
        // 判断当前请求是否可缓存
        if (!this.isRequestCachable(req)) { 
           return next.handle(req); 
        }
        // 获取请求对应的缓存对象,若存在则直接返回该请求对象对应的缓存对象
        const cachedResponse = this.cache.get(req);
        if (cachedResponse !== null) {
           return of(cachedResponse);
        }
        // 发送请求至API站点,请求成功后保存至缓存中
        return next.handle(req).pipe(
           tap(event => {
              if (event instanceof HttpResponse) {
                this.cache.put(req, event); 
              }
           })
        );
    }
    
    // 判断当前请求是否可缓存
    private isRequestCachable(req: HttpRequest<any>) {
        return (req.method === 'GET') && (req.url.indexOf(CACHABLE_URL) > -1);
    }
}

与 LoggingInterceptor 拦截器一样,在使用它之前还需对 CachingInterceptor 进行配置:

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

当应用启动后,我们点击页面上的 Get Users 按钮,此时控制台会输出以下内容:

logger.service.ts:8 Save entry.url response into cache
logger.service.ts:8 GET http://jsonplaceholder.typicode.com/users succeeded in 1296ms

然后在过期前,我们再次点击 Get Users 按钮,这时控制台会输出以下内容:

logger.service.ts:8 req.urlWithParams is Expired: false 
logger.service.ts:8 GET http://jsonplaceholder.typicode.com/users succeeded in 2ms

而等缓存过期后(30 s),我们接着点击 Get Users 按钮,这时控制台会输出以下内容:

req.urlWithParams is Expired: true 
logger.service.ts:8 Save entry.url response into cache
logger.service.ts:8 GET http://jsonplaceholder.typicode.com/users succeeded in 1255ms

通过观察以上的输出内容,我们发现 CachingInterceptor 已经能按照我们的预期正常工作了。此时,我们已经介绍了拦截器三个常见的使用场景,最后我们以 AuthInterceptor 拦截器为例,简单介绍一下如何进行单元测试。

Testing

为了方便演示 AuthInterceptor 拦截器的单元测试,首先我们先来定义一个 UserService 类:

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";

@Injectable()
export class UserService {
  ROOT_URL = `http://jsonplaceholder.typicode.com`;

  constructor(private http: HttpClient) {}

  getUsers() {
    return this.http.get(`${this.ROOT_URL}/users`);
  }
}

接着再定义一个 spec 文件:

import { TestBed } from "@angular/core/testing";
import {
  HttpClientTestingModule,
  HttpTestingController
} from "@angular/common/http/testing";
import { HTTP_INTERCEPTORS } from "@angular/common/http";

import { AuthInterceptor } from "./interceptors/auth.interceptor";
import { UserService } from "./user.service";

describe(`AuthInterceptor`, () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        UserService,
        {
          provide: HTTP_INTERCEPTORS,
          useClass: AuthInterceptor,
          multi: true
        }
      ]
    });

    service = TestBed.get(UserService);
    httpMock = TestBed.get(HttpTestingController);
  });

  it("should Authorization header is iloveangular", () => {
    service.getUsers().subscribe(response => {
      expect(response).toBeTruthy();
    });

    const httpRequest = httpMock.expectOne(
      `http://jsonplaceholder.typicode.com/users`
    );

    expect(httpRequest.request.headers.get("X-CustomAuthHeader")).toBe(
      "iloveangular"
    );
  });
});

在完成 spec 文件的定义之后,我们就可以运行 npm run testng test 命令,运行单元测试了。这里只是简单介绍了如何为 AuthInterceptor 拦截器写单元测试,对于单元测试的同学,建议阅读官方或其他的学习资料。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018/09/01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • AuthInterceptor
  • LoggingInterceptor
  • CachingInterceptor
  • Testing
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档