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
的同一个(或其各级父注入器)注入器中提供拦截器。
最新评论