原文:Insider’s guide into interceptors and HttpClient mechanics in Angular
做者:Max Koretskyi
原技术博文由Max Koretskyi
撰写发布,他目前于 ag-Grid 担任开发大使(Developer Advocate)
译者按:开发大使负责确保其所在的公司认真听取社区的声音并向社区传达他们的行动及目标,其做为社区和公司之间的纽带存在。
译者:Ice Panpan;校对者:dreamdevil00git
您可能知道 Angular 在4.3版本中新引入了强大的 HttpClient。它的一个主要功能是请求拦截(request interception)—— 声明位于应用程序和后端之间的拦截器的能力。拦截器的文档写的很好,展现了如何编写并注册一个拦截器。在这篇文章中,我将深刻研究 HttpClient
服务的内部机制,特别是拦截器。我相信这些知识对于深刻使用该功能是必要的。阅读完本文后,您将可以轻松了解像缓存之类工具的工做流程,并可以轻松自如地实现复杂的请求/响应操做方案。github
首先,咱们将使用文档中描述的方法来注册两个拦截器,以便为请求添加自定义的请求头。而后咱们将实现自定义的中间件链,而不是使用 Angular 定义的机制。最后,咱们将了解 HttpClient
的请求方法如何构建 HttpEvents
类型的 observable 流并知足不可变(immutability)性的需求。json
与个人大部分文章同样,咱们将经过操做实例来学习更多内容。bootstrap
首先,让咱们实现两个简单的拦截器,每一个拦截器使用文档中描述的方法向传出的请求添加请求头。对于每一个拦截器,咱们声明一个实现了 intercept
方法的类。在此方法中,咱们经过添加 Custom-Header-1
和 Custom-Header-2
的请求头信息来修改请求:后端
@Injectable()
export class I1 implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const modified = req.clone({setHeaders: {'Custom-Header-1': '1'}});
return next.handle(modified);
}
}
@Injectable()
export class I2 implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const modified = req.clone({setHeaders: {'Custom-Header-2': '2'}});
return next.handle(modified);
}
}
复制代码
正如您所看到的,每一个拦截器都将下一个处理程序做为第二个参数。咱们须要经过调用该函数来将控制权传递给中间件链中的下一个拦截器。咱们会很快发现调用 next.handle
时发生了什么以及为何有的时候你不须要调用该函数。此外,若是你一直想知道为何须要对请求调用 clone()
方法,你很快就会获得答案。api
拦截器实现以后,咱们须要使用 HTTP_INTERCEPTORS
令牌注册它们:浏览器
@NgModule({
imports: [BrowserModule, HttpClientModule],
declarations: [AppComponent],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: I1,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: I2,
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule {}
复制代码
紧接着执行一个简单的请求来检查咱们自定义的请求头是否添加成功:缓存
@Component({
selector: 'my-app',
template: ` <div><h3>Response</h3>{{response|async|json}}</div> <button (click)="request()">Make request</button>`
,
})
export class AppComponent {
response: Observable<any>;
constructor(private http: HttpClient) {}
request() {
const url = 'https://jsonplaceholder.typicode.com/posts/1';
this.response = this.http.get(url, {observe: 'response'});
}
}
复制代码
若是咱们已经正确完成了全部操做,当咱们检查 Network
选项卡时,咱们能够看到咱们自定义的请求头发送到服务器:服务器
这很容易吧。你能够在 stackblitz 上找到这个基础示例。如今是时候研究更有趣的东西了。网络
咱们的任务是在不使用 HttpClient
提供的方法的状况下手动将拦截器集成处处理请求的逻辑中。同时,咱们将构建一个处理程序链,就像 Angular 内部完成的同样。
在现代浏览器中,AJAX 功能是使用 XmlHttpRequest 或 Fetch API 实现的。此外,还有常用的会致使与变动检测相关的意外结果的 JSONP
技术。Angular 须要一个使用上述方法之一的服务来向服务器发出请求。这种服务在 HttpClient
文档上被称为 后端(backend),例如:
In an interceptor, next always represents the next interceptor in the chain, if any, or the final backend if there are no more interceptors
在拦截器中,
next
始终表示链中的下一个拦截器(若是有的话),若是没有更多拦截器的话则表示最终后端
在 Angular 提供的 HttpClient
模块中,这种服务有两种实现方法——使用 XmlHttpRequest API 实现的HttpXhrBackend 和使用 JSONP 技术实现的 JsonpClientBackend。HttpClient
中默认使用 HttpXhrBackend
。
Angular 定义了一个名为 HTTP(request)handler 的抽象概念,负责处理请求。处理请求的中间件链由 HTTP handlers 组成,这些处理程序将请求传递给链中的下一个处理程序,直到其中一个处理程序返回一个 observable 流。处理程序的接口由抽象类 HttpHandler 定义:
export abstract class HttpHandler {
abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
}
复制代码
因为 backend 服务(如 HttpXhrBackend )能够经过 发出网络请求来处理请求,因此它是 HTTP handler 的一个例子。经过和 backend 服务通讯来处理请求是最多见的处理形式,但却不是惟一的处理方式。另外一种常见的请求处理示例是从本地缓存中为请求提供服务,而不是发送请求给服务器。所以,任何能够处理请求的服务都应该实现 handle
方法,该方法根据函数签名返回一个 HTTP events 类型的 observable,如 HttpProgressEvent
,HttpHeaderResponse
或HttpResponse
。所以,若是咱们想提供一些自定义请求处理逻辑,咱们须要建立一个实现了 HttpHandler
接口的服务。
HttpClient
服务在 DI 容器中的 HttpHandler 令牌下注入了一个全局的 HTTP handler 。而后经过调用它的 handle
方法来发出请求:
export class HttpClient {
constructor(private handler: HttpHandler) {}
request(...): Observable<any> {
...
const events$: Observable<HttpEvent<any>> =
of(req).pipe(concatMap((req: HttpRequest<any>) => this.handler.handle(req)));
...
}
}
复制代码
默认状况下,全局 HTTP handler 是 HttpXhrBackend backend。它被注册在注入器中的 HttpBackend
令牌下。
@NgModule({
providers: [
HttpXhrBackend,
{ provide: HttpBackend, useExisting: HttpXhrBackend }
]
})
export class HttpClientModule {}
复制代码
正如你可能猜到的那样 HttpXhrBackend
实现了 HttpHandler
接口:
export abstract class HttpHandler {
abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
}
export abstract class HttpBackend implements HttpHandler {
abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
}
export class HttpXhrBackend implements HttpBackend {
handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {}
}
复制代码
因为默认的 XHR backend 是在 HttpBackend
令牌下注册的,咱们能够本身注入并替换 HttpClient
用于发出请求的用法。咱们替换掉下面这个使用 HttpClient
的版本:
export class AppComponent {
response: Observable<any>;
constructor(private http: HttpClient) {}
request() {
const url = 'https://jsonplaceholder.typicode.com/posts/1';
this.response = this.http.get(url, {observe: 'body'});
}
}
复制代码
让咱们直接使用默认的 XHR backend,以下所示:
export class AppComponent {
response: Observable<any>;
constructor(private backend: HttpXhrBackend) {}
request() {
const req = new HttpRequest('GET', 'https://jsonplaceholder.typicode.com/posts/1');
this.response = this.backend.handle(req);
}
}
复制代码
这是示例。在示例中须要注意一些事项。首先,咱们须要手动构建 HttpRequest
。其次,因为 backend 处理程序返回 HTTP events 流,你将在屏幕上看到不一样的对象一闪而过,最终将呈现整个 http 响应对象。
咱们已经设法直接使用 backend,但因为咱们没有运行拦截器,因此请求头还没有添加到请求中。一个拦截器包含处理请求的逻辑,但它要与 HttpClient
一块儿使用,须要将其封装到实现了 HttpHandler
接口的服务中。咱们能够经过执行一个拦截器并将链中的下一个处理程序的引用传递给此拦截器的方式来实现此服务。这样拦截器就能够触发下一个处理程序,后者一般是 backend。为此,每一个自定义的处理程序将保存链中下一个处理程序的引用,并将其与请求一块儿传递给下一个拦截器。下面就是咱们想要的东西:
在 Angular 中已经存在这种封装处理程序的方法了并被称为 HttpInterceptorHandler
。让咱们用它来封装咱们的一个拦截器吧。可是不幸的是,Angular 没有将其导出为公共 API,所以咱们只能从源代码中复制基本实现:
export class HttpInterceptorHandler implements HttpHandler {
constructor(private next: HttpHandler, private interceptor: HttpInterceptor) {}
handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
// execute an interceptor and pass the reference to the next handler
return this.interceptor.intercept(req, this.next);
}
}
复制代码
并像这样使用它来封装咱们的第一个拦截器:
export class AppComponent {
response: Observable<any>;
constructor(private backend: HttpXhrBackend) {}
request() {
const req = new HttpRequest('GET', 'https://jsonplaceholder.typicode.com/posts/1');
const handler = new HttpInterceptorHandler(this.backend, new I1());
this.response = handler.handle(req);
}
}
复制代码
如今,一旦咱们发出请求,咱们就能够看到 Custom-Header-1
已添加到请求中。这是示例。经过上面的实现,咱们将一个拦截器和引用了下一个处理程序的 XHR backend 封装进了 HttpInterceptorHandler
。如今,这就是这是一条处理程序链。
让咱们经过封装第二个拦截器来将另外一个处理程序添加到链中:
export class AppComponent {
response: Observable<any>;
constructor(private backend: HttpXhrBackend) {}
request() {
const req = new HttpRequest('GET', 'https://jsonplaceholder.typicode.com/posts/1');
const i1Handler = new HttpInterceptorHandler(this.backend, new I1());
const i2Handler = new HttpInterceptorHandler(i1Handler, new I2());
this.response = i2Handler.handle(req);
}
}
复制代码
在这能够看到演示,如今一切正常,就像咱们在最开始的示例中使用 HttpClient
的那样。咱们刚刚所作的就是构建了处理程序的中间件链,其中每一个处理程序执行一个拦截器并将下一个处理程序的引用传递给它。这是链的图表:
当咱们在拦截器中执行 next.handle(modified)
语句时,咱们将控制权传递给链中的下一个处理程序:
export class I1 implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const modified = req.clone({setHeaders: {'Custom-Header-1': '1'}});
// passing control to the handler in the chain
return next.handle(modified);
}
}
复制代码
最终,控制权将被传递到最后一个 backend 处理程序,该处理程序将对服务器执行请求。
咱们能够经过使用 HTTP_INTERCEPTORS
令牌注入全部的拦截器,而后使用 reduceRight 将它们连接起来的方式自动构建拦截器链,而不是逐个地手动将拦截器连接起来构成拦截器链。咱们这样作:
export class AppComponent {
response: Observable<any>;
constructor(
private backend: HttpBackend,
@Inject(HTTP_INTERCEPTORS) private interceptors: HttpInterceptor[]) {}
request() {
const req = new HttpRequest('GET', 'https://jsonplaceholder.typicode.com/posts/1');
const i2Handler = this.interceptors.reduceRight(
(next, interceptor) => new HttpInterceptorHandler(next, interceptor), this.backend);
this.response = i2Handler.handle(req);
}
}
复制代码
咱们须要在这里使用 reduceRight
来从最后注册的拦截器开始构建一个链。使用上面的代码,咱们会得到与手动构建的处理程序链相同的链。经过 reduceRight
返回的值是对链中第一个处理程序的引用。
实际上,上述我写的代码在 Angular 中是使用 interceptingHandler
函数来实现的。原话是这么说的:
Constructs an
HttpHandler
that applies a bunch ofHttpInterceptor
s to a request before passing it to the givenHttpBackend
. Meant to be used as a factory function withinHttpClientModule
.构造一个 HttpHandler,在将请求传递给给定的 HttpBackend 以前,将一系列 HttpInterceptor 应用于请求。 能够在 HttpClientModule 中用做工厂函数。
(下面顺便贴一下源码:)
export function interceptingHandler( backend: HttpBackend, interceptors: HttpInterceptor[] | null = []): HttpHandler {
if (!interceptors) {
return backend;
}
return interceptors.reduceRight(
(next, interceptor) => new HttpInterceptorHandler(next, interceptor), backend);
}
复制代码
如今咱们知道是如何构造一条处理函数链的了。在 HTTP handler 中须要注意的最后一点是, interceptingHandler
默认为 HttpHandler
:
@NgModule({
providers: [
{
provide: HttpHandler,
useFactory: interceptingHandler,
deps: [HttpBackend, [@Optional(), @Inject(HTTP_INTERCEPTORS)]],
}
]
})
export class HttpClientModule {}
复制代码
所以,执行此函数的结果是链中第一个处理程序的引用被注入 HttpClient
服务并被使用。
好的,如今咱们知道咱们有一堆处理程序,每一个处理程序执行一个关联的拦截器并调用链中的下一个处理程序。调用此链返回的值是一个 HttpEvents
类型的 observable 流。这个流一般(但不老是)由最后一个处理程序生成,这跟 backend 的具体实现有关。其余的处理程序一般只返回该流。下面是大多数拦截器最后的语句:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
...
return next.handle(authReq);
}
复制代码
因此咱们能够这样来展现逻辑:
可是由于任何拦截器均可以返回一个 HttpEvents
类型的 observable 流,因此你有不少定制机会。例如,你能够实现本身的 backend 并将其注册为拦截器。或者实现一个缓存机制,若是找到了缓存就当即返回, 而不用交给下个处理程序处理:
此外,因为每一个拦截器均可以访问下一个拦截器(经过调用 next.handler()
)返回的 observable 流,因此咱们能够经过 RxJs 操做符添加自定义的逻辑来修改返回的流。
若是您仔细阅读了前面的部分,那么您如今可能想知道处理链建立的 HTTP events 流是否与调用 HttpClient
方法,如 get
或者 post
所返回的流彻底相同。咦...不是!实现的过程更有意思。
HttpClient
经过使用 RxJS 的建立操做符 of
来将请求对象变为 observable 流,并在调用 HttpClient
的 HTTP request
方法时返回它。处理程序链做为此流的一部分被同步处理,而且使用 concatMap
操做符压平链返回的 observable。实现的关键点就在 request
方法,由于全部的 API 方法像 get
,post
或 delete
只是包装了 request
方法:
const events$: Observable<HttpEvent<any>> = of(req).pipe(
concatMap((req: HttpRequest<any>) => this.handler.handle(req))
);
复制代码
在上面的代码片断中,我用 pipe
替换了旧技术 call
。若是您仍然对 concatMap
如何工做感到困惑,你能够阅读学习将 RxJs 序列与超级直观的交互式图表相结合。有趣的是,处理程序链在以 of
开头的 observable 流中执行是有缘由的,这里有一个解释:
Start with an Observable.of() the initial request, and run the handler (which includes all interceptors) inside a concatMap(). This way, the handler runs inside an Observable chain, which causes interceptors to be re-run on every subscription (this also makes retries re-run the handler, including interceptors).
经过 Observable.of() 初始请求,并在 concatMap() 中运行处理程序(包括全部拦截器)。这样,处理程序就在一个 Observable 链中运行,这会使得拦截器会在每一个订阅上从新运行(这样重试的时候也会从新运行处理程序,包括拦截器)。
经过 HttpClient
建立的初始 observable 流,发出了全部的 HTTP events,如 HttpProgressEvent
,HttpHeaderResponse
或 HttpResponse
。可是从文档中咱们知道咱们能够经过设置 observe 选项来指定咱们感兴趣的事件:
request() {
const url = 'https://jsonplaceholder.typicode.com/posts/1';
this.response = this.http.get(url, {observe: 'body'});
}
复制代码
使用 {observe: 'body'}
后,从 get
方法返回的 observable 流只会发出响应中 body
部分的内容。 observe
的其余选项还有 events
和 response
而且 response
是默认选项。在探索处理程序链的实现的一开始,我就指出过调用处理程序链返回的流会发出全部 HTTP events。根据 observe
的参数过滤这些 events 是 HttpClient
的责任。
这意味着我在上一节中演示 HttpClient
返回流的实现须要稍微调整一下。咱们须要作的是过滤这些 events 并根据 observe
参数值将它们映射到不一样的值。接下来简单实现下:
const events$: Observable<HttpEvent<any>> = of(req).pipe(...)
if (options.observe === 'events') {
return events$;
}
const res$: Observable<HttpResponse<any>> =
events$.pipe(filter((event: HttpEvent<any>) => event instanceof HttpResponse));
if (options.observe === 'response') {
return res$;
}
if (options.observe === 'body') {
return res$.pipe(map((res: HttpResponse<any>) => res.body));
}
复制代码
在这里,您能够找到源码。
文档上关于不变性的一个有趣的段落是这样的:
Interceptors exist to examine and mutate outgoing requests and incoming responses. However, it may be surprising to learn that the HttpRequest and HttpResponse classes are largely immutable. This is for a reason: because the app may retry requests, the interceptor chain may process an individual request multiple times. If requests were mutable, a retried request would be different than the original request. Immutability ensures the interceptors see the same request for each try.
虽然拦截器有能力改变请求和响应,但 HttpRequest 和 HttpResponse 实例的属性倒是只读(readonly)的,所以,它们在很大意义上说是不可变对象。有充足的理由把它们作成不可变对象:应用可能会重试发送不少次请求以后才能成功,这就意味着这个拦截器链表可能会屡次重复处理同一个请求。 若是拦截器能够修改原始的请求对象,那么重试阶段的操做就会从修改过的请求开始,而不是原始请求。 而这种不可变性,能够确保这些拦截器在每次重试时看到的都是一样的原始请求。
让我详细说明一下。当您调用 HttpClient
的任何 HTTP 请求方法时,就会建立请求对象。正如我在前面部分中解释的那样,此请求用于生成一个 events$
的 observable 序列,而且在订阅时,它会在处理程序链中被传递。可是 events$
流可能会被重试,这意味着在序列以外建立的原始请求对象可能再次触发序列屡次。但拦截器应始终以原始请求开始。若是请求是可变的,而且能够在拦截器运行期间进行修改,则此条件不适用于下一次拦截器运行。因为同一请求对象的引用将屡次用于开始 observable 序列,请求及其全部组成部分,如 HttpHeaders
和 HttpParams
应该是不可变的。