HTTP 的请求/响应任务执行从认证到记日志等很多种隐式任务。 如果没有拦截机制,那么开发人员将不得不对每次 HttpClient 调用显式实现这些任务。比如每次设置请求头中的 token 。编写拦截器
要实现拦截器,就要实现一个实现了 HttpInterceptor 接口中的 intercept() 方法的类。
生成一个 HeroInterceptor:
ng g interceptor http-study/interceptors/hero
默认代码长这样:
// hero.interceptor.ts
import { Injectable } from '@angular/core';
import {HttpRequest, HttpHandler, HttpEvent, HttpInterceptor} from '@angular/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();

// 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 是一个多重提供者的令牌,表示这个令牌可以注入多个拦截器。
删除我们原来在具体方法中设置的请求头,来验证拦截器是否设置成功。

大功告成,我们设置的第一个请求拦截器成功~
同时,我们还可以在响应体中设置统一的错误处理提示:
// 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。 这个拦截器不会对请求与相应做任何修改,原样发送给调用者。(最后别忘了提供这个拦截器)最后我们的效果应该是这样的:
拦截器实现缓存
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 全部功能,记得提供这个拦截器。
用拦截器来请求多个值
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$ 的管道中,使用重组后的可观察对象进行处理,并发出两次。先立即发出一次缓存的响应体,然后发出来自服务器的响应。总结
-
cli工具生成拦截器是没有简写方式的,应该是ng g interceptor xxx; -
拦截器请求顺序是按照配置顺序执行,响应拦截则是相反的顺序; -
用拦截器做请求缓存能优化性能,是个好东西; -
必须在提供 HttpClient的同一个(或其各级父注入器)注入器中提供拦截器。



最新评论