Angular拦截请求和响应

一个多重提供商(multi-provider)令牌,它代表所有已注册的HttpInterceptor构成的数组

const HTTP_INTERCEPTORS: InjectionToken<HttpInterceptor[]>;

什么是HttpInterceptor?

拦截HttpRequest并处理它们

interface HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
}

说明

大多数拦截器都会在外发的请求由next.handle(transformedReq)发给拦截器链中的下一个拦截器之前,对该请求进行转换。

拦截器还可以通过为next.handle()返回的流添加额外的RxJS操作符,来对响应事件流进行转换。

极少数情况下,拦截器也可以自己完全处理一个请求,并且组合出新的事件流来而不必调用next.handle()。

这也是允许的,不过要时刻记住,这将会完全跳过所有后继拦截器。

另一种同样罕见但是有用的拦截器,会为单个请求在事件流上给出多个响应对象。

方法

intercept()

标识和处理给定的HTTP请求

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<>any>

参数

req HttpRequest 要处理的传出请求对象

next HttpHandler 链中的下一个拦截器,如果链中没有拦截器,则成为后端。

返回值

Observable<HttpEvent<any>> 事件流的一个可观察对象

使用说明

要想在整个应用中使用HttpInterceptors的同一个实例,就只能在AppModule模块中导入HttpClientModule,

并且把拦截器都添加到应用到根注入器中。如果你在不同的模块中多次导入HttpClientModule,则每次导入都会创建HttpClientModule的一个新复本,

它将会覆盖根模块上提供的那些拦截器。

拦截请求和响应

参考:https://angular.cn/guide/http#intercepting-requests-and-responses

借助拦截机制,你可以声明一些拦截器,它们可以检查并转换从应用中发给服务器的HTTP请求。这些拦截器还可以在返回应用的途中检查和转换来自服务器的响应。

多个拦截器构成了请求/响应处理器的双向链表。

拦截器可以用一种常规的、标准的方式对每一次HTTP的请求/响应任务执行从认证到记日志等很多种隐式任务。

如果没有拦截机制,那么开发人员将不得不对每次HttpClient调用显示实现这些任务。

编写拦截器

要实现拦截器,就要实现一个实现了HttpInterceptor接口中的intercept()方法的类。

这里是一个什么也不做的空白拦截器,它只会不做任何修改的传递这个请求。

import { Injectable } from '@angular/core';
import {
    HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';

import { Observable } from 'rxjs';

@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对象实现了HttpHandle接口。

export abstract class HttpHandler {
    abstract hande(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开始

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数组中,就像这样:

...
import { httpInterceptorProviders } from './core/http-interceptors';
...

...
  providers: [ httpInterceptorProviders, { provide: NZ_I18N, useValue: zh_CN }],
...

当你再创建新的拦截器时,就同样把它们添加到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的只读属性

/** 错误: Cannot assign to 'url' because it is a read-only property. */
        req.url = req.url.replace('http://', 'https://');

如果你必须修改一个请求,先把它克隆一份,修改这个克隆体后再把它传给next.handle()

你可以在一步中克隆并修改此请求,例子如下:

        const secureReq = req.clone({
            url: req.url.replace('http://', 'https://')
        });
        return next.handle(secureReq);

这个clone()方法的哈希型参数允许你在复制出克隆体的同时改变请求的某些特定属性。

修改请求体

readonly这种赋值保护,无法防范深修改(修改子对象的属性),也不能防范你修改请求体对象中的属性。

req.body.name = req.body.name.trim(); // bad idea!

如果必须修改请求体,请执行以下步骤

1. 复制请求体并在副本中进行修改

2. 使用clone()方法克隆这个请求对象

3. 用修改过的副本替换被克隆的请求体

app/http-interceptors/trim-name-interceptor.ts(except)

        const body = req.body;

        const newBody = { ...body, name: body.name.trim() };
        const newReq = req.clone({ body: newBody });

        return next.handle(newReq);

克隆时清除请求体

有时,你需要清除请求体而不是替换它。为此,请将克隆后的请求体设置为null

提示:如果你把克隆后的请求体设为undefined,那么Angular会认为你想让请求体保持原样。

const newReq = req.clone({body: null});

设置默认请求头

(未完)