Angular9 拦截请求和响应
借助拦截机制,你可以声明一些拦截器,它们可以检查并转换从应用中发给服务器的 HTTP 请求。这些拦截器还可以在返回应用的途中检查和转换来自服务器的响应。多个拦截器构成了请求/响应处理器的双向链表。
拦截器可以用一种常规的、标准的方式对每一次 HTTP 的请求/响应任务执行从认证到记日志等很多种隐式任务。
如果没有拦截机制,那么开发人员将不得不对每次 HttpClient
调用显式实现这些任务。
编写拦截器
要实现拦截器,就要实现一个实现了 HttpInterceptor
接口中的 intercept()
方法的类。
这里是一个什么也不做的空白拦截器,它只会不做任何修改的传递这个请求。
Path:"app/http-interceptors/noop-interceptor.ts" 。
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs';
/** Pass untouched request through to the next request handler. */
@Injectable()
export class NoopInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler):
Observable<HttpEvent<any>> {
return next.handle(req);
}
}
intercept
方法会把请求转换成一个最终返回 HTTP 响应体的 Observable
。 在这个场景中,每个拦截器都完全能自己处理这个请求。
大多数拦截器拦截都会在传入时检查请求,然后把(可能被修改过的)请求转发给 next
对象的 handle()
方法,而 next
对象实现了 HttpHandler
接口。
export abstract class HttpHandler {
abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
}
像 intercept()
一样,handle()
方法也会把 HTTP 请求转换成 HttpEvents
组成的 Observable
,它最终包含的是来自服务器的响应。 intercept()
函数可以检查这个可观察对象,并在把它返回给调用者之前修改它。
这个无操作的拦截器,会直接使用原始的请求调用 next.handle()
,并返回它返回的可观察对象,而不做任何后续处理。
next 对象
next
对象表示拦截器链表中的下一个拦截器。 这个链表中的最后一个 next
对象就是 HttpClient
的后端处理器(backend handler
),它会把请求发给服务器,并接收服务器的响应。
大多数的拦截器都会调用 next.handle()
,以便这个请求流能走到下一个拦截器,并最终传给后端处理器。 拦截器也可以不调用 next.handle()
,使这个链路短路,并返回一个带有人工构造出来的服务器响应的 自己的 Observable
。
这是一种常见的中间件模式,在像 "Express.js" 这样的框架中也会找到它。
提供这个拦截器
这个 NoopInterceptor
就是一个由 Angular 依赖注入 (DI
)系统管理的服务。 像其它服务一样,你也必须先提供这个拦截器类,应用才能使用它。
由于拦截器是 HttpClient
服务的(可选)依赖,所以你必须在提供 HttpClient
的同一个(或其各级父注入器)注入器中提供这些拦截器。 那些在 DI
创建完 HttpClient
之后再提供的拦截器将会被忽略。
由于在 AppModule
中导入了 HttpClientModule
,导致本应用在其根注入器中提供了 HttpClient
。所以你也同样要在 AppModule
中提供这些拦截器。
在从 @angular/common/http
中导入了 HTTP_INTERCEPTORS
注入令牌之后,编写如下的 NoopInterceptor
提供者注册语句:
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
注意 multi: true
选项。 这个必须的选项会告诉 Angular HTTP_INTERCEPTORS
是一个多重提供者的令牌,表示它会注入一个多值的数组,而不是单一的值。
你也可以直接把这个提供者添加到 AppModule
中的提供者数组中,不过那样会非常啰嗦。况且,你将来还会用这种方式创建更多的拦截器并提供它们。 你还要特别注意提供这些拦截器的顺序。
认真考虑创建一个封装桶(barrel
)文件,用于把所有拦截器都收集起来,一起提供给 httpInterceptorProviders
数组,可以先从这个 NoopInterceptor
开始。
Path:"app/http-interceptors/index.ts" 。
/* "Barrel" of Http Interceptors */
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NoopInterceptor } from './noop-interceptor';
/** Http interceptor providers in outside-in order */
export const httpInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
];
然后导入它,并把它加到 AppModule
的 providers
数组中,就像这样:
Path:"app/app.module.ts (interceptor providers)" 。
providers: [
httpInterceptorProviders
],
当你再创建新的拦截器时,就同样把它们添加到 httpInterceptorProviders
数组中,而不用再修改 AppModule
。
拦截器的顺序
Angular 会按照你提供它们的顺序应用这些拦截器。 如果你提供拦截器的顺序是先 A,再 B,再 C,那么请求阶段的执行顺序就是 A->B->C,而响应阶段的执行顺序则是 C->B->A。
以后你就再也不能修改这些顺序或移除某些拦截器了。 如果你需要动态启用或禁用某个拦截器,那就要在那个拦截器中自行实现这个功能。
处理拦截器事件
大多数 HttpClient
方法都会返回 HttpResponse<any>
型的可观察对象。HttpResponse
类本身就是一个事件,它的类型是 HttpEventType.Response
。但是,单个 HTTP
请求可以生成其它类型的多个事件,包括报告上传和下载进度的事件。HttpInterceptor.intercept()
和 HttpHandler.handle()
会返回 HttpEvent<any>
型的可观察对象。
很多拦截器只关心发出的请求,而对 next.handle()
返回的事件流不会做任何修改。 但是,有些拦截器需要检查并修改 next.handle()
的响应。上述做法就可以在流中看到所有这些事件。
虽然拦截器有能力改变请求和响应,但 HttpRequest
和 HttpResponse
实例的属性却是只读(readonly
)的, 因此让它们基本上是不可变的。
有充足的理由把它们做成不可变对象:应用可能会重试发送很多次请求之后才能成功,这就意味着这个拦截器链表可能会多次重复处理同一个请求。 如果拦截器可以修改原始的请求对象,那么重试阶段的操作就会从修改过的请求开始,而不是原始请求。 而这种不可变性,可以确保这些拦截器在每次重试时看到的都是同样的原始请求。
你的拦截器应该在没有任何修改的情况下返回每一个事件,除非它有令人信服的理由去做。
TypeScript 会阻止你设置 HttpRequest
的只读属性。
// Typescript disallows the following assignment because req.url is readonly
req.url = req.url.replace('http://', 'https://');
如果你必须修改一个请求,先把它克隆一份,修改这个克隆体后再把它传给 next.handle()
。你可以在一步中克隆并修改此请求,例子如下。
Path:"app/http-interceptors/ensure-https-interceptor.ts (excerpt)" 。
// clone request and replace 'http://' with 'https://' at the same time
const secureReq = req.clone({
url: req.url.replace('http://', 'https://')
});
// send the cloned, "secure" request to the next handler.
return next.handle(secureReq);
这个 clone()
方法的哈希型参数允许你在复制出克隆体的同时改变该请求的某些特定属性。
- 修改请求体。
readonly
这种赋值保护,无法防范深修改(修改子对象的属性),也不能防范你修改请求体对象中的属性。
req.body.name = req.body.name.trim(); // bad idea!
如果必须修改请求体,请执行以下步骤。
- 复制请求体并在副本中进行修改。
- 使用
clone()
方法克隆这个请求对象。
- 用修改过的副本替换被克隆的请求体。
// copy the body and trim whitespace from the name property
const newBody = { ...body, name: body.name.trim() };
// clone request and set its body
const newReq = req.clone({ body: newBody });
// send the cloned request to the next handler.
return next.handle(newReq);
- 克隆时清除请求体。
有时,你需要清除请求体而不是替换它。为此,请将克隆后的请求体设置为 null
。
注:
- 如果你把克隆后的请求体设为
undefined
,那么 Angular 会认为你想让请求体保持原样。
newReq = req.clone({ ... }); // body not mentioned => preserve original body
newReq = req.clone({ body: undefined }); // preserve original body
newReq = req.clone({ body: null }); // clear the body
设置默认请求头
应用通常会使用拦截器来设置外发请求的默认请求头。
该范例应用具有一个 AuthService
,它会生成一个认证令牌。 在这里,AuthInterceptor
会注入该服务以获取令牌,并对每一个外发的请求添加一个带有该令牌的认证头:
Path:"app/http-interceptors/auth-interceptor.ts" 。
import { AuthService } from '../auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
// Get the auth token from the service.
const authToken = this.auth.getAuthorizationToken();
// Clone the request and replace the original headers with
// cloned headers, updated with the authorization.
const authReq = req.clone({
headers: req.headers.set('Authorization', authToken)
});
// send cloned request with header to the next handler.
return next.handle(authReq);
}
}
这种在克隆请求的同时设置新请求头的操作太常见了,因此它还有一个快捷方式 setHeaders
:
// Clone the request and set the new header in one step.
const authReq = req.clone({ setHeaders: { Authorization: authToken } });
这种可以修改头的拦截器可以用于很多不同的操作,比如:
- 认证 / 授权
- 控制缓存行为。比如
If-Modified-Since
- XSRF 防护
用拦截器记日志
因为拦截器可以同时处理请求和响应,所以它们也可以对整个 HTTP 操作执行计时和记录日志等任务。
考虑下面这个 LoggingInterceptor
,它捕获请求的发起时间、响应的接收时间,并使用注入的 MessageService
来发送总共花费的时间。
Path:"app/http-interceptors/logging-interceptor.ts)" 。
import { finalize, tap } from 'rxjs/operators';
import { MessageService } from '../message.service';
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
constructor(private messenger: MessageService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
const started = Date.now();
let ok: string;
// extend server response observable with logging
return next.handle(req)
.pipe(
tap(
// Succeeds when there is a response; ignore other events
event => ok = event instanceof HttpResponse ? 'succeeded' : '',
// Operation failed; error is an HttpErrorResponse
error => ok = 'failed'
),
// Log when response observable either completes or errors
finalize(() => {
const elapsed = Date.now() - started;
const msg = `${req.method} "${req.urlWithParams}"
${ok} in ${elapsed} ms.`;
this.messenger.add(msg);
})
);
}
}
RxJS 的 tap
操作符会捕获请求成功了还是失败了。 RxJS 的 finalize
操作符无论在响应成功还是失败时都会调用(这是必须的),然后把结果汇报给 MessageService
。
在这个可观察对象的流中,无论是 tap
还是 finalize
接触过的值,都会照常发送给调用者。
用拦截器实现缓存
拦截器还可以自行处理这些请求,而不用转发给 next.handle()
。
比如,你可能会想缓存某些请求和响应,以便提升性能。 你可以把这种缓存操作委托给某个拦截器,而不破坏你现有的各个数据服务。
下例中的 CachingInterceptor
演示了这种方法。
Path:"app/http-interceptors/caching-interceptor.ts)" 。
@Injectable()
export class CachingInterceptor implements HttpInterceptor {
constructor(private cache: RequestCache) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
// continue if not cacheable.
if (!isCacheable(req)) { return next.handle(req); }
const cachedResponse = this.cache.get(req);
return cachedResponse ?
of(cachedResponse) : sendRequest(req, next, this.cache);
}
}
isCacheable()
函数用于决定该请求是否允许缓存。 在这个例子中,只有发到npm
包搜索API
的GET
请求才是可以缓存的。
- 如果该请求是不可缓存的,该拦截器只会把该请求转发给链表中的下一个处理器。
- 如果可缓存的请求在缓存中找到了,该拦截器就会通过
of()
函数返回一个已缓存的响应体的可观察对象,然后绕过next
处理器(以及所有其它下游拦截器)。
- 如果可缓存的请求不在缓存中,代码会调用
sendRequest()
。这个函数会创建一个没有请求头的请求克隆体,这是因为npm API
禁止它们。然后,该函数把请求的克隆体转发给next.handle()
,它会最终调用服务器并返回来自服务器的响应对象。
/**
* Get server response observable by sending request to `next()`.
* Will add the response to the cache on the way out.
*/
function sendRequest(
req: HttpRequest<any>,
next: HttpHandler,
cache: RequestCache): Observable<HttpEvent<any>> {
// No headers allowed in npm search request
const noHeaderReq = req.clone({ headers: new HttpHeaders() });
return next.handle(noHeaderReq).pipe(
tap(event => {
// There may be other events besides the response.
if (event instanceof HttpResponse) {
cache.put(req, event); // Update the cache.
}
})
);
}
注意 sendRequest()
是如何在返回应用程序的过程中拦截响应的。该方法通过 tap()
操作符来管理响应对象,该操作符的回调函数会把该响应对象添加到缓存中。
然后,原始的响应会通过这些拦截器链,原封不动的回到服务器的调用者那里。
数据服务,比如 PackageSearchService
,并不知道它们收到的某些 HttpClient
请求实际上是从缓存的请求中返回来的。
用拦截器来请求多个值
HttpClient.get()
方法通常会返回一个可观察对象,它会发出一个值(数据或错误)。拦截器可以把它改成一个可以发出多个值的可观察对象。
修改后的 CachingInterceptor
版本可以返回一个立即发出所缓存响应的可观察对象,然后把请求发送到 NPM
的 Web API
,然后把修改过的搜索结果重新发出一次。
// cache-then-refresh
if (req.headers.get('x-refresh')) {
const results$ = sendRequest(req, next, this.cache);
return cachedResponse ?
results$.pipe( startWith(cachedResponse) ) :
results$;
}
// cache-or-fetch
return cachedResponse ?
of(cachedResponse) : sendRequest(req, next, this.cache);
cache-then-refresh
选项是由一个自定义的x-refresh
请求头触发的。
PackageSearchComponent
中的一个检查框会切换withRefresh
标识, 它是PackageSearchService.search()
的参数之一。search()
方法创建了自定义的x-refresh
头,并在调用HttpClient.get()
前把它添加到请求里。
修改后的 CachingInterceptor
会发起一个服务器请求,而不管有没有缓存的值。 就像 前面 的 sendRequest()
方法一样进行订阅。 在订阅 results$
可观察对象时,就会发起这个请求。
- 如果没有缓存值,拦截器直接返回
results$
。
- 如果有缓存的值,这些代码就会把缓存的响应加入到
result$
的管道中,使用重组后的可观察对象进行处理,并发出两次。 先立即发出一次缓存的响应体,然后发出来自服务器的响应。 订阅者将会看到一个包含这两个响应的序列。
更多建议: