Angular9 攔截請(qǐng)求和響應(yīng)

2020-07-06 15:25 更新

借助攔截機(jī)制,你可以聲明一些攔截器,它們可以檢查并轉(zhuǎn)換從應(yīng)用中發(fā)給服務(wù)器的 HTTP 請(qǐng)求。這些攔截器還可以在返回應(yīng)用的途中檢查和轉(zhuǎn)換來自服務(wù)器的響應(yīng)。多個(gè)攔截器構(gòu)成了請(qǐng)求/響應(yīng)處理器的雙向鏈表。

攔截器可以用一種常規(guī)的、標(biāo)準(zhǔn)的方式對(duì)每一次 HTTP 的請(qǐng)求/響應(yīng)任務(wù)執(zhí)行從認(rèn)證到記日志等很多種隱式任務(wù)。

如果沒有攔截機(jī)制,那么開發(fā)人員將不得不對(duì)每次 HttpClient 調(diào)用顯式實(shí)現(xiàn)這些任務(wù)。

編寫攔截器

要實(shí)現(xiàn)攔截器,就要實(shí)現(xiàn)一個(gè)實(shí)現(xiàn)了 HttpInterceptor 接口中的 intercept() 方法的類。

這里是一個(gè)什么也不做的空白攔截器,它只會(huì)不做任何修改的傳遞這個(gè)請(qǐng)求。

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 方法會(huì)把請(qǐng)求轉(zhuǎn)換成一個(gè)最終返回 HTTP 響應(yīng)體的 Observable。 在這個(gè)場景中,每個(gè)攔截器都完全能自己處理這個(gè)請(qǐng)求。

大多數(shù)攔截器攔截都會(huì)在傳入時(shí)檢查請(qǐng)求,然后把(可能被修改過的)請(qǐng)求轉(zhuǎn)發(fā)給 next 對(duì)象的 handle() 方法,而 next 對(duì)象實(shí)現(xiàn)了 HttpHandler 接口。

export abstract class HttpHandler {
  abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
}

intercept() 一樣,handle() 方法也會(huì)把 HTTP 請(qǐng)求轉(zhuǎn)換成 HttpEvents 組成的 Observable,它最終包含的是來自服務(wù)器的響應(yīng)。 intercept() 函數(shù)可以檢查這個(gè)可觀察對(duì)象,并在把它返回給調(diào)用者之前修改它。

這個(gè)無操作的攔截器,會(huì)直接使用原始的請(qǐng)求調(diào)用 next.handle(),并返回它返回的可觀察對(duì)象,而不做任何后續(xù)處理。

next 對(duì)象

next 對(duì)象表示攔截器鏈表中的下一個(gè)攔截器。 這個(gè)鏈表中的最后一個(gè) next 對(duì)象就是 HttpClient 的后端處理器(backend handler),它會(huì)把請(qǐng)求發(fā)給服務(wù)器,并接收服務(wù)器的響應(yīng)。

大多數(shù)的攔截器都會(huì)調(diào)用 next.handle(),以便這個(gè)請(qǐng)求流能走到下一個(gè)攔截器,并最終傳給后端處理器。 攔截器也可以不調(diào)用 next.handle(),使這個(gè)鏈路短路,并返回一個(gè)帶有人工構(gòu)造出來的服務(wù)器響應(yīng)的 自己的 Observable。

這是一種常見的中間件模式,在像 "Express.js" 這樣的框架中也會(huì)找到它。

提供這個(gè)攔截器

這個(gè) NoopInterceptor 就是一個(gè)由 Angular 依賴注入 (DI)系統(tǒng)管理的服務(wù)。 像其它服務(wù)一樣,你也必須先提供這個(gè)攔截器類,應(yīng)用才能使用它。

由于攔截器是 HttpClient 服務(wù)的(可選)依賴,所以你必須在提供 HttpClient 的同一個(gè)(或其各級(jí)父注入器)注入器中提供這些攔截器。 那些在 DI 創(chuàng)建完 HttpClient 之后再提供的攔截器將會(huì)被忽略。

由于在 AppModule 中導(dǎo)入了 HttpClientModule,導(dǎo)致本應(yīng)用在其根注入器中提供了 HttpClient。所以你也同樣要在 AppModule 中提供這些攔截器。

在從 @angular/common/http 中導(dǎo)入了 HTTP_INTERCEPTORS 注入令牌之后,編寫如下的 NoopInterceptor 提供者注冊(cè)語句:

{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },

注意 multi: true 選項(xiàng)。 這個(gè)必須的選項(xiàng)會(huì)告訴 Angular HTTP_INTERCEPTORS 是一個(gè)多重提供者的令牌,表示它會(huì)注入一個(gè)多值的數(shù)組,而不是單一的值。

你也可以直接把這個(gè)提供者添加到 AppModule 中的提供者數(shù)組中,不過那樣會(huì)非常啰嗦。況且,你將來還會(huì)用這種方式創(chuàng)建更多的攔截器并提供它們。 你還要特別注意提供這些攔截器的順序。

認(rèn)真考慮創(chuàng)建一個(gè)封裝桶(barrel)文件,用于把所有攔截器都收集起來,一起提供給 httpInterceptorProviders 數(shù)組,可以先從這個(gè) 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 },
];

然后導(dǎo)入它,并把它加到 AppModuleproviders 數(shù)組中,就像這樣:

Path:"app/app.module.ts (interceptor providers)" 。

providers: [
  httpInterceptorProviders
],

當(dāng)你再創(chuàng)建新的攔截器時(shí),就同樣把它們添加到 httpInterceptorProviders 數(shù)組中,而不用再修改 AppModule。

攔截器的順序

Angular 會(huì)按照你提供它們的順序應(yīng)用這些攔截器。 如果你提供攔截器的順序是先 A,再 B,再 C,那么請(qǐng)求階段的執(zhí)行順序就是 A->B->C,而響應(yīng)階段的執(zhí)行順序則是 C->B->A。

以后你就再也不能修改這些順序或移除某些攔截器了。 如果你需要?jiǎng)討B(tài)啟用或禁用某個(gè)攔截器,那就要在那個(gè)攔截器中自行實(shí)現(xiàn)這個(gè)功能。

處理攔截器事件

大多數(shù) HttpClient 方法都會(huì)返回 HttpResponse<any> 型的可觀察對(duì)象。HttpResponse 類本身就是一個(gè)事件,它的類型是 HttpEventType.Response。但是,單個(gè) HTTP 請(qǐng)求可以生成其它類型的多個(gè)事件,包括報(bào)告上傳和下載進(jìn)度的事件。HttpInterceptor.intercept()HttpHandler.handle() 會(huì)返回 HttpEvent<any> 型的可觀察對(duì)象。

很多攔截器只關(guān)心發(fā)出的請(qǐng)求,而對(duì) next.handle() 返回的事件流不會(huì)做任何修改。 但是,有些攔截器需要檢查并修改 next.handle() 的響應(yīng)。上述做法就可以在流中看到所有這些事件。

雖然攔截器有能力改變請(qǐng)求和響應(yīng),但 HttpRequestHttpResponse 實(shí)例的屬性卻是只讀(readonly)的, 因此讓它們基本上是不可變的。

有充足的理由把它們做成不可變對(duì)象:應(yīng)用可能會(huì)重試發(fā)送很多次請(qǐng)求之后才能成功,這就意味著這個(gè)攔截器鏈表可能會(huì)多次重復(fù)處理同一個(gè)請(qǐng)求。 如果攔截器可以修改原始的請(qǐng)求對(duì)象,那么重試階段的操作就會(huì)從修改過的請(qǐng)求開始,而不是原始請(qǐng)求。 而這種不可變性,可以確保這些攔截器在每次重試時(shí)看到的都是同樣的原始請(qǐng)求。

你的攔截器應(yīng)該在沒有任何修改的情況下返回每一個(gè)事件,除非它有令人信服的理由去做。

TypeScript 會(huì)阻止你設(shè)置 HttpRequest 的只讀屬性。

// Typescript disallows the following assignment because req.url is readonly
req.url = req.url.replace('http://', 'https://');

如果你必須修改一個(gè)請(qǐng)求,先把它克隆一份,修改這個(gè)克隆體后再把它傳給 next.handle()。你可以在一步中克隆并修改此請(qǐng)求,例子如下。

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

這個(gè) clone() 方法的哈希型參數(shù)允許你在復(fù)制出克隆體的同時(shí)改變?cè)撜?qǐng)求的某些特定屬性。

  1. 修改請(qǐng)求體。

readonly 這種賦值保護(hù),無法防范深修改(修改子對(duì)象的屬性),也不能防范你修改請(qǐng)求體對(duì)象中的屬性。

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

如果必須修改請(qǐng)求體,請(qǐng)執(zhí)行以下步驟。

  • 復(fù)制請(qǐng)求體并在副本中進(jìn)行修改。

  • 使用 clone() 方法克隆這個(gè)請(qǐng)求對(duì)象。

  • 用修改過的副本替換被克隆的請(qǐng)求體。

    // 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. 克隆時(shí)清除請(qǐng)求體。

有時(shí),你需要清除請(qǐng)求體而不是替換它。為此,請(qǐng)將克隆后的請(qǐng)求體設(shè)置為 null。

注:

  • 如果你把克隆后的請(qǐng)求體設(shè)為 undefined,那么 Angular 會(huì)認(rèn)為你想讓請(qǐng)求體保持原樣。

    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

設(shè)置默認(rèn)請(qǐng)求頭

應(yīng)用通常會(huì)使用攔截器來設(shè)置外發(fā)請(qǐng)求的默認(rèn)請(qǐng)求頭。

該范例應(yīng)用具有一個(gè) AuthService,它會(huì)生成一個(gè)認(rèn)證令牌。 在這里,AuthInterceptor 會(huì)注入該服務(wù)以獲取令牌,并對(duì)每一個(gè)外發(fā)的請(qǐng)求添加一個(gè)帶有該令牌的認(rèn)證頭:

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

這種在克隆請(qǐng)求的同時(shí)設(shè)置新請(qǐng)求頭的操作太常見了,因此它還有一個(gè)快捷方式 setHeaders

// Clone the request and set the new header in one step.
const authReq = req.clone({ setHeaders: { Authorization: authToken } });

這種可以修改頭的攔截器可以用于很多不同的操作,比如:

  • 認(rèn)證 / 授權(quán)

  • 控制緩存行為。比如 If-Modified-Since

  • XSRF 防護(hù)

用攔截器記日志

因?yàn)閿r截器可以同時(shí)處理請(qǐng)求和響應(yīng),所以它們也可以對(duì)整個(gè) HTTP 操作執(zhí)行計(jì)時(shí)和記錄日志等任務(wù)。

考慮下面這個(gè) LoggingInterceptor,它捕獲請(qǐng)求的發(fā)起時(shí)間、響應(yīng)的接收時(shí)間,并使用注入的 MessageService 來發(fā)送總共花費(fèi)的時(shí)間。

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 操作符會(huì)捕獲請(qǐng)求成功了還是失敗了。 RxJS 的 finalize 操作符無論在響應(yīng)成功還是失敗時(shí)都會(huì)調(diào)用(這是必須的),然后把結(jié)果匯報(bào)給 MessageService。

在這個(gè)可觀察對(duì)象的流中,無論是 tap 還是 finalize 接觸過的值,都會(huì)照常發(fā)送給調(diào)用者。

用攔截器實(shí)現(xiàn)緩存

攔截器還可以自行處理這些請(qǐng)求,而不用轉(zhuǎn)發(fā)給 next.handle()

比如,你可能會(huì)想緩存某些請(qǐng)求和響應(yīng),以便提升性能。 你可以把這種緩存操作委托給某個(gè)攔截器,而不破壞你現(xiàn)有的各個(gè)數(shù)據(jù)服務(wù)。

下例中的 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() 函數(shù)用于決定該請(qǐng)求是否允許緩存。 在這個(gè)例子中,只有發(fā)到 npm 包搜索 APIGET 請(qǐng)求才是可以緩存的。

  • 如果該請(qǐng)求是不可緩存的,該攔截器只會(huì)把該請(qǐng)求轉(zhuǎn)發(fā)給鏈表中的下一個(gè)處理器。

  • 如果可緩存的請(qǐng)求在緩存中找到了,該攔截器就會(huì)通過 of() 函數(shù)返回一個(gè)已緩存的響應(yīng)體的可觀察對(duì)象,然后繞過 next 處理器(以及所有其它下游攔截器)。

  • 如果可緩存的請(qǐng)求不在緩存中,代碼會(huì)調(diào)用 sendRequest()。這個(gè)函數(shù)會(huì)創(chuàng)建一個(gè)沒有請(qǐng)求頭的請(qǐng)求克隆體,這是因?yàn)?npm API 禁止它們。然后,該函數(shù)把請(qǐng)求的克隆體轉(zhuǎn)發(fā)給 next.handle(),它會(huì)最終調(diào)用服務(wù)器并返回來自服務(wù)器的響應(yīng)對(duì)象。

/**
 * 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() 是如何在返回應(yīng)用程序的過程中攔截響應(yīng)的。該方法通過 tap() 操作符來管理響應(yīng)對(duì)象,該操作符的回調(diào)函數(shù)會(huì)把該響應(yīng)對(duì)象添加到緩存中。

然后,原始的響應(yīng)會(huì)通過這些攔截器鏈,原封不動(dòng)的回到服務(wù)器的調(diào)用者那里。

數(shù)據(jù)服務(wù),比如 PackageSearchService,并不知道它們收到的某些 HttpClient 請(qǐng)求實(shí)際上是從緩存的請(qǐng)求中返回來的。

用攔截器來請(qǐng)求多個(gè)值

HttpClient.get() 方法通常會(huì)返回一個(gè)可觀察對(duì)象,它會(huì)發(fā)出一個(gè)值(數(shù)據(jù)或錯(cuò)誤)。攔截器可以把它改成一個(gè)可以發(fā)出多個(gè)值的可觀察對(duì)象。

修改后的 CachingInterceptor 版本可以返回一個(gè)立即發(fā)出所緩存響應(yīng)的可觀察對(duì)象,然后把請(qǐng)求發(fā)送到 NPMWeb API,然后把修改過的搜索結(jié)果重新發(fā)出一次。

// 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 選項(xiàng)是由一個(gè)自定義的 x-refresh 請(qǐng)求頭觸發(fā)的。

PackageSearchComponent 中的一個(gè)檢查框會(huì)切換 withRefresh 標(biāo)識(shí), 它是 PackageSearchService.search() 的參數(shù)之一。 search() 方法創(chuàng)建了自定義的 x-refresh 頭,并在調(diào)用 HttpClient.get() 前把它添加到請(qǐng)求里。

修改后的 CachingInterceptor 會(huì)發(fā)起一個(gè)服務(wù)器請(qǐng)求,而不管有沒有緩存的值。 就像 前面 的 sendRequest() 方法一樣進(jìn)行訂閱。 在訂閱 results$ 可觀察對(duì)象時(shí),就會(huì)發(fā)起這個(gè)請(qǐng)求。

  • 如果沒有緩存值,攔截器直接返回 results$

  • 如果有緩存的值,這些代碼就會(huì)把緩存的響應(yīng)加入到 result$ 的管道中,使用重組后的可觀察對(duì)象進(jìn)行處理,并發(fā)出兩次。 先立即發(fā)出一次緩存的響應(yīng)體,然后發(fā)出來自服務(wù)器的響應(yīng)。 訂閱者將會(huì)看到一個(gè)包含這兩個(gè)響應(yīng)的序列。
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)