Angular9 拦截请求和响应

2020-07-06 15:25 更新

借助拦截机制,你可以声明一些拦截器,它们可以检查并转换从应用中发给服务器的 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 },
];

然后导入它,并把它加到 AppModuleproviders 数组中,就像这样:

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() 的响应。上述做法就可以在流中看到所有这些事件。

虽然拦截器有能力改变请求和响应,但 HttpRequestHttpResponse 实例的属性却是只读(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() 方法的哈希型参数允许你在复制出克隆体的同时改变该请求的某些特定属性。

  1. 修改请求体。

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);

  1. 克隆时清除请求体。

有时,你需要清除请求体而不是替换它。为此,请将克隆后的请求体设置为 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 包搜索 APIGET 请求才是可以缓存的。

  • 如果该请求是不可缓存的,该拦截器只会把该请求转发给链表中的下一个处理器。

  • 如果可缓存的请求在缓存中找到了,该拦截器就会通过 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 版本可以返回一个立即发出所缓存响应的可观察对象,然后把请求发送到 NPMWeb 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$ 的管道中,使用重组后的可观察对象进行处理,并发出两次。 先立即发出一次缓存的响应体,然后发出来自服务器的响应。 订阅者将会看到一个包含这两个响应的序列。
以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号