在之前的 Angular 6 HttpClient 快速入门 文章中,我们已经简单介绍了 Http 拦截器。本文将会进一步分析一下 Http 拦截器。拦截器提供了一种用于拦截、修改请求和响应的机制。这个概念与 Node.js 的 Express 框架中间件的概念类似。拦截器提供的这种特性,对于日志、缓存、请求授权来说非常有用。
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(),该方法接收两个参数:
在上面的 AuthInterceptor 拦截器中,我们实现的功能就是设置自定义请求头。接下来我们来介绍如何利用拦截器实现请求日志记录的功能。
下面我们来定义 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
好的,趁热打铁,我们再来一个例子,即介绍如何利用拦截器实现简单的缓存控制。
在实现缓存拦截器之前,我们先来定义一个 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 接口中,包含两个方法:
另外在实际的场景中,我们一般都会为缓存设置一个最大的缓存时间,即缓存的有效期。在有效期内,如果缓存命中,则会直接返回已缓存的响应对象。下面我们再来定义一个 CacheEntry 接口,该接口包含三个属性:
此外,我们还要定义一个常量,用于设定缓存的有效期,这里我们假设缓存的时间为 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 拦截器为例,简单介绍一下如何进行单元测试。
为了方便演示 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 test
或 ng test
命令,运行单元测试了。这里只是简单介绍了如何为 AuthInterceptor 拦截器写单元测试,对于单元测试的同学,建议阅读官方或其他的学习资料。