Angular HTTP客戶端

2022-07-09 17:21 更新

使用 HTTP 與后端服務(wù)進(jìn)行通信

大多數(shù)前端應(yīng)用都要通過(guò) HTTP 協(xié)議與服務(wù)器通訊,才能下載或上傳數(shù)據(jù)并訪問(wèn)其它后端服務(wù)。Angular 給應(yīng)用提供了一個(gè) HTTP 客戶端 API,也就是 @angular/common/http 中的 HttpClient 服務(wù)類(lèi)。

HTTP 客戶端服務(wù)提供了以下主要功能。

  • 請(qǐng)求類(lèi)型化響應(yīng)對(duì)象的能力
  • 簡(jiǎn)化的錯(cuò)誤處理
  • 可測(cè)試性特性
  • 請(qǐng)求和響應(yīng)攔截

服務(wù)器通訊的準(zhǔn)備工作

要想使用 ?HttpClient?,就要先導(dǎo)入 Angular 的 ?HttpClientModule?。大多數(shù)應(yīng)用都會(huì)在根模塊 ?AppModule ?中導(dǎo)入它。

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [
    BrowserModule,
    // import HttpClientModule after BrowserModule.
    HttpClientModule,
  ],
  declarations: [
    AppComponent,
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {}

然后,你可以把 ?HttpClient ?服務(wù)注入成一個(gè)應(yīng)用類(lèi)的依賴項(xiàng),如下面的 ?ConfigService ?例子所示。

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class ConfigService {
  constructor(private http: HttpClient) { }
}

?HttpClient ?服務(wù)為所有工作都使用了可觀察對(duì)象。你必須導(dǎo)入范例代碼片段中出現(xiàn)的 RxJS 可觀察對(duì)象和操作符。比如 ?ConfigService ?中的這些導(dǎo)入就很典型。

import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
你可以運(yùn)行本指南附帶的現(xiàn)場(chǎng)演練 / 下載范例
該范例應(yīng)用不需要數(shù)據(jù)服務(wù)器。它依賴于 Angular-in-memory-web-api,它替代了 HttpClient 模塊中的 ?HttpBackend?。這個(gè)替代服務(wù)會(huì)模擬 REST 式的后端的行為。
看一下 ?AppModule ?的這些導(dǎo)入,看看它的配置方式。

從服務(wù)器請(qǐng)求數(shù)據(jù)

使用 ?HttpClient.get()? 方法從服務(wù)器獲取數(shù)據(jù)。該異步方法會(huì)發(fā)送一個(gè) HTTP 請(qǐng)求,并返回一個(gè) Observable,它會(huì)在收到響應(yīng)時(shí)發(fā)出所請(qǐng)求到的數(shù)據(jù)。返回的類(lèi)型取決于你調(diào)用時(shí)傳入的 ?observe ?和 ?responseType ?參數(shù)。

?get()? 方法有兩個(gè)參數(shù)。要獲取的端點(diǎn) URL,以及一個(gè)可以用來(lái)配置請(qǐng)求的選項(xiàng)對(duì)象。

options: {
  headers?: HttpHeaders | {[header: string]: string | string[]},
  observe?: 'body' | 'events' | 'response',
  params?: HttpParams|{[param: string]: string | number | boolean | ReadonlyArray<string | number | boolean>},
  reportProgress?: boolean,
  responseType?: 'arraybuffer'|'blob'|'json'|'text',
  withCredentials?: boolean,
}

這些重要的選項(xiàng)包括 observe 和 responseType 屬性。

  • observe 選項(xiàng)用于指定要返回的響應(yīng)內(nèi)容。
  • responseType 選項(xiàng)指定返回?cái)?shù)據(jù)的格式。
可以用 ?options ?對(duì)象來(lái)配置傳出請(qǐng)求的各個(gè)方面。比如,在?Adding headers? 中,該服務(wù)使用 ?headers ?選項(xiàng)屬性設(shè)置默認(rèn)頭。
使用 ?params ?屬性可以配置帶HTTP URL 參數(shù)的請(qǐng)求,?reportProgress ?選項(xiàng)可以在傳輸大量數(shù)據(jù)時(shí)監(jiān)聽(tīng)進(jìn)度事件。

應(yīng)用經(jīng)常會(huì)從服務(wù)器請(qǐng)求 JSON 數(shù)據(jù)。在 ?ConfigService ?例子中,該應(yīng)用需要服務(wù)器 ?config.json? 上的一個(gè)配置文件來(lái)指定資源的 URL。

{
  "heroesUrl": "api/heroes",
  "textfile": "assets/textfile.txt",
  "date": "2020-01-29"
}

要獲取這類(lèi)數(shù)據(jù),?get()? 調(diào)用需要以下幾個(gè)選項(xiàng):?{observe: 'body', responseType: 'json'}?。這些是這些選項(xiàng)的默認(rèn)值,所以下面的例子不會(huì)傳遞 options 對(duì)象。后面幾節(jié)展示了一些額外的選項(xiàng)。

這個(gè)例子符合通過(guò)定義一個(gè)可復(fù)用的可注入服務(wù)來(lái)執(zhí)行數(shù)據(jù)處理功能來(lái)創(chuàng)建可伸縮解決方案的最佳實(shí)踐。除了提取數(shù)據(jù)外,該服務(wù)還可以對(duì)數(shù)據(jù)進(jìn)行后處理,添加錯(cuò)誤處理,并添加重試邏輯。

?ConfigService ?使用 ?HttpClient.get()? 方法獲取這個(gè)文件。

configUrl = 'assets/config.json';

getConfig() {
  return this.http.get<Config>(this.configUrl);
}

?ConfigComponent ?注入了 ?ConfigService ?并調(diào)用了 ?getConfig ?服務(wù)方法。

由于該服務(wù)方法返回了一個(gè) ?Observable ?配置數(shù)據(jù),該組件會(huì)訂閱該方法的返回值。訂閱回調(diào)只會(huì)對(duì)后處理進(jìn)行最少量的處理。它會(huì)把數(shù)據(jù)字段復(fù)制到組件的 ?config ?對(duì)象中,該對(duì)象在組件模板中是數(shù)據(jù)綁定的,用于顯示。

showConfig() {
  this.configService.getConfig()
    .subscribe((data: Config) => this.config = {
        heroesUrl: data.heroesUrl,
        textfile:  data.textfile,
        date: data.date,
    });
}

請(qǐng)求輸入一個(gè)類(lèi)型的響應(yīng)

可以構(gòu)造自己的 ?HttpClient ?請(qǐng)求來(lái)聲明響應(yīng)對(duì)象的類(lèi)型,以便讓輸出更容易、更明確。所指定的響應(yīng)類(lèi)型會(huì)在編譯時(shí)充當(dāng)類(lèi)型斷言。

指定響應(yīng)類(lèi)型是在向 TypeScript 聲明,它應(yīng)該把你的響應(yīng)對(duì)象當(dāng)做給定類(lèi)型來(lái)使用。這是一種構(gòu)建期檢查,它并不能保證服務(wù)器會(huì)實(shí)際給出這種類(lèi)型的響應(yīng)對(duì)象。該服務(wù)器需要自己確保返回服務(wù)器 API 中指定的類(lèi)型。

要指定響應(yīng)對(duì)象類(lèi)型,首先要定義一個(gè)具有必需屬性的接口。這里要使用接口而不是類(lèi),因?yàn)轫憫?yīng)對(duì)象是普通對(duì)象,無(wú)法自動(dòng)轉(zhuǎn)換成類(lèi)的實(shí)例。

export interface Config {
  heroesUrl: string;
  textfile: string;
  date: any;
}

接下來(lái),在服務(wù)器中把該接口指定為 ?HttpClient.get()? 調(diào)用的類(lèi)型參數(shù)。

getConfig() {
  // now returns an Observable of Config
  return this.http.get<Config>(this.configUrl);
}

當(dāng)把接口作為類(lèi)型參數(shù)傳給 ?HttpClient.get()? 方法時(shí),可以使用RxJS map 操作符來(lái)根據(jù) UI 的需求轉(zhuǎn)換響應(yīng)數(shù)據(jù)。然后,把轉(zhuǎn)換后的數(shù)據(jù)傳給異步管道。

修改后的組件方法,其回調(diào)函數(shù)中獲取一個(gè)帶類(lèi)型的對(duì)象,它易于使用,且消費(fèi)起來(lái)更安全:

config: Config | undefined;

showConfig() {
  this.configService.getConfig()
    // clone the data object, using its known Config shape
    .subscribe((data: Config) => this.config = { ...data });
}

要訪問(wèn)接口中定義的屬性,必須將從 JSON 獲得的普通對(duì)象顯式轉(zhuǎn)換為所需的響應(yīng)類(lèi)型。比如,以下 ?subscribe ?回調(diào)會(huì)將 ?data ?作為對(duì)象接收,然后進(jìn)行類(lèi)型轉(zhuǎn)換以訪問(wèn)屬性。

.subscribe(data => this.config = {
  heroesUrl: (data as any).heroesUrl,
  textfile:  (data as any).textfile,
});
OBSERVE 和 RESPONSE 的類(lèi)型
?observe ?和 ?response ?選項(xiàng)的類(lèi)型是字符串的聯(lián)合類(lèi)型,而不是普通的字符串。
options: {
  …
  observe?: 'body' | 'events' | 'response',
  …
  responseType?: 'arraybuffer'|'blob'|'json'|'text',
  …
}
這會(huì)引起混亂。比如:
// this works
client.get('/foo', {responseType: 'text'})

// but this does NOT work
const options = {
  responseType: 'text',
};
client.get('/foo', options)
在第二種情況下,TypeScript 會(huì)把 ?options ?的類(lèi)型推斷為 ?{responseType: string}?。該類(lèi)型的 ?HttpClient.get? 太寬泛,無(wú)法傳給 ?HttpClient.get?,它希望 ?responseType ?的類(lèi)型是特定的字符串之一。而 ?HttpClient ?就是以這種方式顯式輸入的,因此編譯器可以根據(jù)你提供的選項(xiàng)報(bào)告正確的返回類(lèi)型。  

使用 ?as const?,可以讓 TypeScript 知道你并不是真的要使用字面字符串類(lèi)型:

const options = {
  responseType: 'text' as const,
};
client.get('/foo', options);

讀取完整的響應(yīng)體

在前面的例子中,對(duì) ?HttpClient.get()? 的調(diào)用沒(méi)有指定任何選項(xiàng)。默認(rèn)情況下,它返回了響應(yīng)體中包含的 JSON 數(shù)據(jù)。

你可能還需要關(guān)于這次對(duì)話的更多信息。比如,有時(shí)候服務(wù)器會(huì)返回一個(gè)特殊的響應(yīng)頭或狀態(tài)碼,來(lái)指出某些在應(yīng)用的工作流程中很重要的條件。

可以用 ?get()? 方法的 ?observe ?選項(xiàng)來(lái)告訴 ?HttpClient?,你想要完整的響應(yīng)對(duì)象:

getConfigResponse(): Observable<HttpResponse<Config>> {
  return this.http.get<Config>(
    this.configUrl, { observe: 'response' });
}

現(xiàn)在,?HttpClient.get()? 會(huì)返回一個(gè) ?HttpResponse ?類(lèi)型的 ?Observable?,而不只是 JSON 數(shù)據(jù)。

該組件的 ?showConfigResponse()? 方法會(huì)像顯示配置數(shù)據(jù)一樣顯示響應(yīng)頭:

showConfigResponse() {
  this.configService.getConfigResponse()
    // resp is of type `HttpResponse<Config>`
    .subscribe(resp => {
      // display its headers
      const keys = resp.headers.keys();
      this.headers = keys.map(key =>
        `${key}: ${resp.headers.get(key)}`);

      // access the body directly, which is typed as `Config`.
      this.config = { ...resp.body! };
    });
}

如你所見(jiàn),該響應(yīng)對(duì)象具有一個(gè)帶有正確類(lèi)型的 ?body ?屬性。

發(fā)起 JSONP 請(qǐng)求

當(dāng)服務(wù)器不支持 CORS 協(xié)議時(shí),應(yīng)用程序可以使用 ?HttpClient ?跨域發(fā)出 ?JSONP ?請(qǐng)求。

Angular 的 JSONP 請(qǐng)求會(huì)返回一個(gè) ?Observable?。遵循訂閱可觀察對(duì)象變量的模式,并在使用 async 管道管理結(jié)果之前,使用 RxJS ?map ?操作符轉(zhuǎn)換響應(yīng)。

在 Angular 中,通過(guò)在 ?NgModule ?的 ?imports ?中包含 ?HttpClientJsonpModule ?來(lái)使用 JSONP。在以下范例中,?searchHeroes()? 方法使用 JSONP 請(qǐng)求來(lái)查詢名稱包含搜索詞的英雄。

/* GET heroes whose name contains search term */
searchHeroes(term: string): Observable {
  term = term.trim();

  const heroesURL = `${this.heroesURL}?${term}`;
  return this.http.jsonp(heroesUrl, 'callback').pipe(
      catchError(this.handleError('searchHeroes', [])) // then handle the error
    );
}

該請(qǐng)求將 ?heroesURL ?作為第一個(gè)參數(shù),并將回調(diào)函數(shù)名稱作為第二個(gè)參數(shù)。響應(yīng)被包裝在回調(diào)函數(shù)中,該函數(shù)接受 JSONP 方法返回的可觀察對(duì)象,并將它們通過(guò)管道傳給錯(cuò)誤處理程序。

請(qǐng)求非 JSON 數(shù)據(jù)

不是所有的 API 都會(huì)返回 JSON 數(shù)據(jù)。在下面這個(gè)例子中,?DownloaderService ?中的方法會(huì)從服務(wù)器讀取文本文件,并把文件的內(nèi)容記錄下來(lái),然后把這些內(nèi)容使用 ?Observable<string>? 的形式返回給調(diào)用者。

getTextFile(filename: string) {
  // The Observable returned by get() is of type Observable<string>
  // because a text response was specified.
  // There's no need to pass a <string> type parameter to get().
  return this.http.get(filename, {responseType: 'text'})
    .pipe(
      tap( // Log the result or error
      {
        next: (data) => this.log(filename, data),
        error: (error) => this.logError(filename, error)
      }
      )
    );
}

這里的 ?HttpClient.get()? 返回字符串而不是默認(rèn)的 JSON 對(duì)象,因?yàn)樗?nbsp;?responseType ?選項(xiàng)是 ?'text'?。

RxJS 的 ?tap ?操作符(如“竊聽(tīng)”中所述)使代碼可以檢查通過(guò)可觀察對(duì)象的成功值和錯(cuò)誤值,而不會(huì)干擾它們。

在 ?DownloaderComponent ?中的 ?download()? 方法通過(guò)訂閱這個(gè)服務(wù)中的方法來(lái)發(fā)起一次請(qǐng)求。

download() {
  this.downloaderService.getTextFile('assets/textfile.txt')
    .subscribe(results => this.contents = results);
}

處理請(qǐng)求錯(cuò)誤

如果請(qǐng)求在服務(wù)器上失敗了,那么 ?HttpClient ?就會(huì)返回一個(gè)錯(cuò)誤對(duì)象而不是一個(gè)成功的響應(yīng)對(duì)象。

執(zhí)行服務(wù)器請(qǐng)求的同一個(gè)服務(wù)中也應(yīng)該執(zhí)行錯(cuò)誤檢查、解釋和解析。

發(fā)生錯(cuò)誤時(shí),你可以獲取失敗的詳細(xì)信息,以便通知你的用戶。在某些情況下,你也可以自動(dòng)重試該請(qǐng)求

獲取錯(cuò)誤詳情

當(dāng)數(shù)據(jù)訪問(wèn)失敗時(shí),應(yīng)用會(huì)給用戶提供有用的反饋。原始的錯(cuò)誤對(duì)象作為反饋并不是特別有用。除了檢測(cè)到錯(cuò)誤已經(jīng)發(fā)生之外,還需要獲取錯(cuò)誤詳細(xì)信息并使用這些細(xì)節(jié)來(lái)撰寫(xiě)用戶友好的響應(yīng)。

可能會(huì)出現(xiàn)兩種類(lèi)型的錯(cuò)誤。

  • 服務(wù)器端可能會(huì)拒絕該請(qǐng)求,并返回狀態(tài)碼為 404 或 500 的 HTTP 響應(yīng)對(duì)象。這些是錯(cuò)誤響應(yīng)
  • 客戶端也可能出現(xiàn)問(wèn)題,比如網(wǎng)絡(luò)錯(cuò)誤會(huì)讓請(qǐng)求無(wú)法成功完成,或者 RxJS 操作符也會(huì)拋出異常。這些錯(cuò)誤會(huì)產(chǎn)生 JavaScript 的 ?ErrorEvent ?對(duì)象。這些錯(cuò)誤的 ?status ?為 ?0?,并且其 ?error ?屬性包含一個(gè) ?ProgressEvent ?對(duì)象,此對(duì)象的 ?type ?屬性可以提供更詳細(xì)的信息。

?HttpClient ?在其 ?HttpErrorResponse ?中會(huì)捕獲兩種錯(cuò)誤??梢詸z查這個(gè)響應(yīng)是否存在錯(cuò)誤。

下面的例子在之前定義的 ?ConfigService ?中定義了一個(gè)錯(cuò)誤處理程序。

private handleError(error: HttpErrorResponse) {
  if (error.status === 0) {
    // A client-side or network error occurred. Handle it accordingly.
    console.error('An error occurred:', error.error);
  } else {
    // The backend returned an unsuccessful response code.
    // The response body may contain clues as to what went wrong.
    console.error(
      `Backend returned code ${error.status}, body was: `, error.error);
  }
  // Return an observable with a user-facing error message.
  return throwError(() => new Error('Something bad happened; please try again later.'));
}

該處理程序會(huì)返回一個(gè)帶有用戶友好的錯(cuò)誤信息的 RxJS ?ErrorObservable?。下列代碼修改了 ?getConfig()? 方法,它使用一個(gè)管道把 ?HttpClient.get()? 調(diào)用返回的所有 Observable 發(fā)送給錯(cuò)誤處理器。

getConfig() {
  return this.http.get<Config>(this.configUrl)
    .pipe(
      catchError(this.handleError)
    );
}

重試失敗的請(qǐng)求

有時(shí)候,錯(cuò)誤只是臨時(shí)性的,只要重試就可能會(huì)自動(dòng)消失。比如,在移動(dòng)端場(chǎng)景中可能會(huì)遇到網(wǎng)絡(luò)中斷的情況,只要重試一下就能拿到正確的結(jié)果。

RxJS 庫(kù)提供了幾個(gè)重試操作符。比如,?retry()? 操作符會(huì)自動(dòng)重新訂閱一個(gè)失敗的 ?Observable ?幾次。重新訂閱 ?HttpClient ?方法會(huì)導(dǎo)致它重新發(fā)出 HTTP 請(qǐng)求。

下面的例子演示了如何在把一個(gè)失敗的請(qǐng)求傳給錯(cuò)誤處理程序之前,先通過(guò)管道傳給 ?retry()? 操作符。

getConfig() {
  return this.http.get<Config>(this.configUrl)
    .pipe(
      retry(3), // retry a failed request up to 3 times
      catchError(this.handleError) // then handle the error
    );
}

把數(shù)據(jù)發(fā)送到服務(wù)器

除了從服務(wù)器獲取數(shù)據(jù)外,?HttpClient ?還支持其它一些 HTTP 方法,比如 PUT,POST 和 DELETE,你可以用它們來(lái)修改遠(yuǎn)程數(shù)據(jù)。

本指南中的這個(gè)范例應(yīng)用包括一個(gè)簡(jiǎn)略版本的《英雄之旅》,它會(huì)獲取英雄數(shù)據(jù),并允許用戶添加、刪除和修改它們。下面幾節(jié)在 ?HeroesService ?范例中展示了數(shù)據(jù)更新方法的一些例子。

發(fā)起一個(gè) POST 請(qǐng)求

應(yīng)用經(jīng)常在提交表單時(shí)通過(guò) POST 請(qǐng)求向服務(wù)器發(fā)送數(shù)據(jù)。下面這個(gè)例子中,?HeroesService ?在向數(shù)據(jù)庫(kù)添加英雄時(shí)發(fā)起了一個(gè) HTTP POST 請(qǐng)求。

/** POST: add a new hero to the database */
addHero(hero: Hero): Observable<Hero> {
  return this.http.post<Hero>(this.heroesUrl, hero, httpOptions)
    .pipe(
      catchError(this.handleError('addHero', hero))
    );
}

?HttpClient.post()? 方法像 ?get()? 一樣也有類(lèi)型參數(shù),可以用它來(lái)指出你期望服務(wù)器返回特定類(lèi)型的數(shù)據(jù)。該方法需要一個(gè)資源 URL 和兩個(gè)額外的參數(shù):

參數(shù)

詳情

body

要在請(qǐng)求正文中 POST 的數(shù)據(jù)。

options

一個(gè)包含方法選項(xiàng)的對(duì)象,在這里,它用來(lái)指定必要的請(qǐng)求頭。

這個(gè)例子捕獲了前面獲取錯(cuò)誤詳情所講的錯(cuò)誤。

?HeroesComponent ?通過(guò)訂閱該服務(wù)方法返回的 ?Observable ?發(fā)起了一次實(shí)際的 ?POST ?操作。

this.heroesService
  .addHero(newHero)
  .subscribe(hero => this.heroes.push(hero));

當(dāng)服務(wù)器成功做出響應(yīng)時(shí),會(huì)帶有這個(gè)新創(chuàng)建的英雄,然后該組件就會(huì)把這個(gè)英雄添加到正在顯示的 ?heroes ?列表中。

發(fā)起 DELETE 請(qǐng)求

該應(yīng)用可以把英雄的 ID 傳給 ?HttpClient.delete? 方法的請(qǐng)求 URL 來(lái)刪除一個(gè)英雄。

/** DELETE: delete the hero from the server */
deleteHero(id: number): Observable<unknown> {
  const url = `${this.heroesUrl}/${id}`; // DELETE api/heroes/42
  return this.http.delete(url, httpOptions)
    .pipe(
      catchError(this.handleError('deleteHero'))
    );
}

當(dāng) ?HeroesComponent ?訂閱了該服務(wù)方法返回的 ?Observable ?時(shí),就會(huì)發(fā)起一次實(shí)際的 ?DELETE ?操作。

this.heroesService
  .deleteHero(hero.id)
  .subscribe();

該組件不會(huì)等待刪除操作的結(jié)果,所以它的 subscribe(訂閱)中沒(méi)有回調(diào)函數(shù)。不過(guò)就算你不關(guān)心結(jié)果,也仍然要訂閱它。調(diào)用 ?subscribe()? 方法會(huì)執(zhí)行這個(gè)可觀察對(duì)象,這時(shí)才會(huì)真的發(fā)起 DELETE 請(qǐng)求。

你必須調(diào)用 ?subscribe()?,否則什么都不會(huì)發(fā)生。僅僅調(diào)用 ?HeroesService.deleteHero()? 是不會(huì)發(fā)起 DELETE 請(qǐng)求的。

// oops ... subscribe() is missing so nothing happens
this.heroesService.deleteHero(hero.id);

別忘了訂閱

在調(diào)用方法返回的可觀察對(duì)象的 ?subscribe()? 方法之前,?HttpClient ?方法不會(huì)發(fā)起 HTTP 請(qǐng)求。這適用于 ?HttpClient ?的所有方法。

?AsyncPipe ?會(huì)自動(dòng)為你訂閱(以及取消訂閱)。

?HttpClient ?的所有方法返回的可觀察對(duì)象都設(shè)計(jì)為冷的。HTTP 請(qǐng)求的執(zhí)行都是延期執(zhí)行的,讓你可以用 ?tap ?和 ?catchError ?這樣的操作符來(lái)在實(shí)際執(zhí)行 HTTP 請(qǐng)求之前,先對(duì)這個(gè)可觀察對(duì)象進(jìn)行擴(kuò)展。

調(diào)用 ?subscribe(…)? 會(huì)觸發(fā)這個(gè)可觀察對(duì)象的執(zhí)行,并導(dǎo)致 ?HttpClient ?組合并把 HTTP 請(qǐng)求發(fā)給服務(wù)器。

可以把這些可觀察對(duì)象看做實(shí)際 HTTP 請(qǐng)求的藍(lán)圖。

實(shí)際上,每個(gè) subscribe() 都會(huì)初始化此可觀察對(duì)象的一次單獨(dú)的、獨(dú)立的執(zhí)行。訂閱兩次就會(huì)導(dǎo)致發(fā)起兩個(gè) HTTP 請(qǐng)求。

const req = http.get<Heroes>('/api/heroes');
// 0 requests made - .subscribe() not called.
req.subscribe();
// 1 request made.
req.subscribe();
// 2 requests made.

發(fā)起 PUT 請(qǐng)求

應(yīng)用可以使用 HttpClient 服務(wù)發(fā)送 PUT 請(qǐng)求。下面的 ?HeroesService ?范例(就像 POST 范例一樣)用一個(gè)修改過(guò)的數(shù)據(jù)替換了該資源。

/** PUT: update the hero on the server. Returns the updated hero upon success. */
updateHero(hero: Hero): Observable<Hero> {
  return this.http.put<Hero>(this.heroesUrl, hero, httpOptions)
    .pipe(
      catchError(this.handleError('updateHero', hero))
    );
}

對(duì)于所有返回可觀察對(duì)象的 HTTP 方法,調(diào)用者(?HeroesComponent.update()?)必須 ?subscribe()? 從 ?HttpClient.put()? 返回的可觀察對(duì)象,才會(huì)真的發(fā)起請(qǐng)求。

添加和更新請(qǐng)求頭

很多服務(wù)器都需要額外的頭來(lái)執(zhí)行保存操作。比如,服務(wù)器可能需要一個(gè)授權(quán)令牌,或者需要 ?Content-Type? 頭來(lái)顯式聲明請(qǐng)求體的 MIME 類(lèi)型。

添加請(qǐng)求頭

?HeroesService ?在一個(gè) ?httpOptions ?對(duì)象中定義了這樣的頭,它們被傳給每個(gè) ?HttpClient ?的保存型方法。

import { HttpHeaders } from '@angular/common/http';

const httpOptions = {
  headers: new HttpHeaders({
    'Content-Type':  'application/json',
    Authorization: 'my-auth-token'
  })
};

更新請(qǐng)求頭

你不能直接修改前面的選項(xiàng)對(duì)象中的 ?HttpHeaders ?請(qǐng)求頭,因?yàn)?nbsp;?HttpHeaders ?類(lèi)的實(shí)例是不可變對(duì)象。請(qǐng)改用 ?set()? 方法,以返回當(dāng)前實(shí)例應(yīng)用了新更改之后的副本。

下面的例子演示了當(dāng)舊令牌過(guò)期時(shí),可以在發(fā)起下一個(gè)請(qǐng)求之前更新授權(quán)頭。

httpOptions.headers =
  httpOptions.headers.set('Authorization', 'my-new-auth-token');

配置 HTTP URL 參數(shù)

使用 ?HttpParams ?類(lèi)和 ?params ?選項(xiàng)在你的 ?HttpRequest ?中添加 URL 查詢字符串。

下面的例子中,?searchHeroes()? 方法用于查詢名字中包含搜索詞的英雄。

首先導(dǎo)入 ?HttpParams ?類(lèi)。

import {HttpParams} from "@angular/common/http";
/* GET heroes whose name contains search term */
searchHeroes(term: string): Observable<Hero[]> {
  term = term.trim();

  // Add safe, URL encoded search parameter if there is a search term
  const options = term ?
   { params: new HttpParams().set('name', term) } : {};

  return this.http.get<Hero[]>(this.heroesUrl, options)
    .pipe(
      catchError(this.handleError<Hero[]>('searchHeroes', []))
    );
}

如果有搜索詞,代碼會(huì)用進(jìn)行過(guò) URL 編碼的搜索參數(shù)來(lái)構(gòu)造一個(gè) options 對(duì)象。比如,如果搜索詞是 "cat",那么 GET 請(qǐng)求的 URL 就是 ?api/heroes?name=cat?。

?HttpParams ?是不可變對(duì)象。如果需要更新選項(xiàng),請(qǐng)保留 ?.set()? 方法的返回值。

你也可以使用 ?fromString ?變量從查詢字符串中直接創(chuàng)建 HTTP 參數(shù):

const params = new HttpParams({fromString: 'name=foo'});

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

借助攔截機(jī)制,你可以聲明一些攔截器,它們可以檢查并轉(zhuǎn)換從應(yīng)用中發(fā)給服務(wù)器的 HTTP 請(qǐng)求。這些攔截器還可以在返回應(yīng)用的途中檢查和轉(zhuǎn)換來(lái)自服務(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ù)。

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

編寫(xiě)攔截器

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

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

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è)場(chǎng)景中,每個(gè)攔截器都完全能自己處理這個(gè)請(qǐng)求。

大多數(shù)攔截器攔截都會(huì)在傳入時(shí)檢查請(qǐng)求,然后把(可能被修改過(guò)的)請(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?,它最終包含的是來(lái)自服務(wù)器的響應(yīng)。 ?intercept()? 函數(shù)可以檢查這個(gè)可觀察對(duì)象,并在把它返回給調(diào)用者之前修改它。

這個(gè) ?no-op? 攔截器,會(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)造出來(lái)的服務(wù)器響應(yīng)的 自己的 ?Observable?。

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

提供這個(gè)攔截器

這個(gè) ?NoopInterceptor ?就是一個(gè)由 Angular 依賴注入 (DI)系統(tǒng)管理的服務(wù)。像其它服務(wù)一樣,你也必須先提供這個(gè)攔截器類(lèi),應(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 ?注入令牌之后,編寫(xiě)如下的 ?NoopInterceptor ?提供者注冊(cè)語(yǔ)句:

{ 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ù)組中,不過(guò)那樣會(huì)非常啰嗦。況且,你將來(lái)還會(huì)用這種方式創(chuàng)建更多的攔截器并提供它們。 你還要特別注意提供這些攔截器的順序。

認(rèn)真考慮創(chuàng)建一個(gè)封裝桶(barrel)文件,用于把所有攔截器都收集起來(lái),一起提供給 ?httpInterceptorProviders ?數(shù)組,可以先從這個(gè) ?NoopInterceptor ?開(kāi)始。

/* "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)入它,并把它加到 ?AppModule ?的 ?providers array? 中,就像這樣:

providers: [
  httpInterceptorProviders
],

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

攔截器的順序

Angular 會(huì)按你提供攔截器的順序應(yīng)用它們。比如,考慮一個(gè)場(chǎng)景:你想處理 HTTP 請(qǐng)求的身份驗(yàn)證并記錄它們,然后再將它們發(fā)送到服務(wù)器。要完成此任務(wù),你可以提供 ?AuthInterceptor ?服務(wù),然后提供 ?LoggingInterceptor ?服務(wù)。發(fā)出的請(qǐng)求將從 ?AuthInterceptor ?到 ?LoggingInterceptor?。這些請(qǐng)求的響應(yīng)則沿相反的方向流動(dòng),從 ?LoggingInterceptor ?回到 ?AuthInterceptor?。以下是該過(guò)程的直觀表示:

interceptor-order

該過(guò)程中的最后一個(gè)攔截器始終是處理與服務(wù)器通信的 ?HttpBackend ?服務(wù)。

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

處理攔截器事件

大多數(shù) ?HttpClient ?方法都會(huì)返回 ?HttpResponse<any>? 型的可觀察對(duì)象。?HttpResponse ?類(lèi)本身就是一個(gè)事件,它的類(lèi)型是 ?HttpEventType.Response?。但是,單個(gè) HTTP 請(qǐng)求可以生成其它類(lèi)型的多個(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),但 ?HttpRequest ?和 ?HttpResponse ?實(shí)例的屬性卻是只讀(?readonly?)的, 因此讓它們基本上是不可變的。

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

你的攔截器應(yīng)該在沒(méi)有任何修改的情況下返回每一個(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)求,例子如下。

// 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)求的某些特定屬性。

修改請(qǐng)求體

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

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

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

  1. 復(fù)制請(qǐng)求體并在副本中進(jìn)行修改。
  2. 使用 ?clone()? 方法克隆這個(gè)請(qǐng)求對(duì)象。
  3. 用修改過(guò)的副本替換被克隆的請(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);

克隆時(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

HTTP 攔截器用例

以下是攔截器的一些常見(jiàn)用法。

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

應(yīng)用通常會(huì)使用攔截器來(lái)設(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)證頭:

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)求頭的操作太常見(jiàn)了,因此它還有一個(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ù)

記錄請(qǐng)求與響應(yīng)對(duì)

因?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 ?來(lái)發(fā)送總共花費(fèi)的時(shí)間。

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
          next: (event) => (ok = event instanceof HttpResponse ? 'succeeded' : ''),
          // Operation failed; error is an HttpErrorResponse
          error: (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 ?操作符無(wú)論在響應(yīng)成功還是失敗時(shí)都會(huì)調(diào)用(這是必須的),然后把結(jié)果匯報(bào)給 ?MessageService?。

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

自定義 JSON 解析

攔截器可用來(lái)以自定義實(shí)現(xiàn)替換內(nèi)置的 JSON 解析。

以下示例中的 ?CustomJsonInterceptor ?演示了如何實(shí)現(xiàn)此目的。如果截獲的請(qǐng)求期望一個(gè) ?'json'? 響應(yīng),則將 ?responseType ?更改為 ?'text'? 以禁用內(nèi)置的 JSON 解析。然后,通過(guò)注入的 ?JsonParser ?解析響應(yīng)。

// The JsonParser class acts as a base class for custom parsers and as the DI token.
@Injectable()
export abstract class JsonParser {
  abstract parse(text: string): any;
}

@Injectable()
export class CustomJsonInterceptor implements HttpInterceptor {
  constructor(private jsonParser: JsonParser) {}

  intercept(httpRequest: HttpRequest<any>, next: HttpHandler) {
    if (httpRequest.responseType === 'json') {
      // If the expected response type is JSON then handle it here.
      return this.handleJsonResponse(httpRequest, next);
    } else {
      return next.handle(httpRequest);
    }
  }

  private handleJsonResponse(httpRequest: HttpRequest<any>, next: HttpHandler) {
    // Override the responseType to disable the default JSON parsing.
    httpRequest = httpRequest.clone({responseType: 'text'});
    // Handle the response using the custom parser.
    return next.handle(httpRequest).pipe(map(event => this.parseJsonResponse(event)));
  }

  private parseJsonResponse(event: HttpEvent<any>) {
    if (event instanceof HttpResponse && typeof event.body === 'string') {
      return event.clone({body: this.jsonParser.parse(event.body)});
    } else {
      return event;
    }
  }
}

然后,你可以實(shí)現(xiàn)自己的自定義 ?JsonParser?。這是一個(gè)具有特殊日期接收器的自定義 JsonParser。

@Injectable()
export class CustomJsonParser implements JsonParser {
  parse(text: string): any {
    return JSON.parse(text, dateReviver);
  }
}

function dateReviver(key: string, value: any) {
  /* . . . */
}

你提供 ?CustomParser ?以及 ?CustomJsonInterceptor?。

{ provide: HTTP_INTERCEPTORS, useClass: CustomJsonInterceptor, multi: true },
{ provide: JsonParser, useClass: CustomJsonParser },

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

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

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

下例中的 ?CachingInterceptor ?演示了這種方法。

@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 包搜索 API 的 GET 請(qǐng)求才是可以緩存的。
  • 如果該請(qǐng)求是不可緩存的,該攔截器會(huì)把該請(qǐng)求轉(zhuǎn)發(fā)給鏈表中的下一個(gè)處理器
  • 如果可緩存的請(qǐng)求在緩存中找到了,該攔截器就會(huì)通過(guò) ?of()? 函數(shù)返回一個(gè)已緩存的響應(yīng)體的可觀察對(duì)象,然后繞過(guò) ?next ?處理器(以及所有其它下游攔截器)
  • 如果可緩存的請(qǐng)求不在緩存中,代碼會(huì)調(diào)用 ?sendRequest()?。這個(gè)函數(shù)會(huì)把請(qǐng)求轉(zhuǎn)發(fā)給 ?next.handle()?,它會(huì)最終調(diào)用服務(wù)器并返回來(lái)自服務(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>> {
  return next.handle(req).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)用程序的過(guò)程中攔截響應(yīng)的。該方法通過(guò) ?tap()? 操作符來(lái)管理響應(yīng)對(duì)象,該操作符的回調(diào)函數(shù)會(huì)把該響應(yīng)對(duì)象添加到緩存中。
然后,原始的響應(yīng)會(huì)通過(guò)這些攔截器鏈,原封不動(dòng)的回到服務(wù)器的調(diào)用者那里。
數(shù)據(jù)服務(wù),比如 ?PackageSearchService?,并不知道它們收到的某些 ?HttpClient ?請(qǐng)求實(shí)際上是從緩存的請(qǐng)求中返回來(lái)的。

用攔截器來(lái)請(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ā)送到 NPM 的 Web API,然后把修改過(guò)的搜索結(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)求,而不管有沒(méi)有緩存的值。 就像前面的 ?sendRequest()? 方法一樣進(jìn)行訂閱。 在訂閱 ?results$? 可觀察對(duì)象時(shí),就會(huì)發(fā)起這個(gè)請(qǐng)求。

  • 如果沒(méi)有緩存值,攔截器直接返回 ?results$?。
  • 如果有緩存的值,這些代碼就會(huì)把緩存的響應(yīng)加入到 ?result$? 的管道中,使用重組后的可觀察對(duì)象進(jìn)行處理,并發(fā)出兩次。先立即發(fā)出一次緩存的響應(yīng)體,然后發(fā)出來(lái)自服務(wù)器的響應(yīng)。訂閱者將會(huì)看到一個(gè)包含這兩個(gè)響應(yīng)的序列。

跟蹤和顯示請(qǐng)求進(jìn)度

應(yīng)用程序有時(shí)會(huì)傳輸大量數(shù)據(jù),而這些傳輸可能要花很長(zhǎng)時(shí)間。文件上傳就是典型的例子。你可以通過(guò)提供關(guān)于此類(lèi)傳輸?shù)倪M(jìn)度反饋,為用戶提供更好的體驗(yàn)。

要想發(fā)出一個(gè)帶有進(jìn)度事件的請(qǐng)求,你可以創(chuàng)建一個(gè) ?HttpRequest ?實(shí)例,并把 ?reportProgress ?選項(xiàng)設(shè)置為 true 來(lái)啟用對(duì)進(jìn)度事件的跟蹤。

const req = new HttpRequest('POST', '/upload/file', file, {
  reportProgress: true
});
提示:
每個(gè)進(jìn)度事件都會(huì)觸發(fā)變更檢測(cè),所以只有當(dāng)需要在 UI 上報(bào)告進(jìn)度時(shí),你才應(yīng)該開(kāi)啟它們。
當(dāng) ?HttpClient.request()? 和 HTTP 方法一起使用時(shí),可以用 ?observe: 'events'? 來(lái)查看所有事件,包括傳輸?shù)倪M(jìn)度。

接下來(lái),把這個(gè)請(qǐng)求對(duì)象傳給 ?HttpClient.request()? 方法,該方法返回一個(gè) ?HttpEvents ?的 ?Observable?。

// The `HttpClient.request` API produces a raw event stream
// which includes start (sent), progress, and response events.
return this.http.request(req).pipe(
  map(event => this.getEventMessage(event, file)),
  tap(message => this.showProgress(message)),
  last(), // return last (completed) message to caller
  catchError(this.handleError(file))
);

?getEventMessage ?方法解釋了事件流中每種類(lèi)型的 ?HttpEvent?。

/** Return distinct message for sent, upload progress, & response events */
private getEventMessage(event: HttpEvent<any>, file: File) {
  switch (event.type) {
    case HttpEventType.Sent:
      return `Uploading file "${file.name}" of size ${file.size}.`;

    case HttpEventType.UploadProgress:
      // Compute and show the % done:
      const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0;
      return `File "${file.name}" is ${percentDone}% uploaded.`;

    case HttpEventType.Response:
      return `File "${file.name}" was completely uploaded!`;

    default:
      return `File "${file.name}" surprising upload event: ${event.type}.`;
  }
}

本指南中的范例應(yīng)用中沒(méi)有用來(lái)接受上傳文件的服務(wù)器。?app/http-interceptors/upload-interceptor.ts? 的 ?UploadInterceptor ?通過(guò)返回一個(gè)模擬這些事件的可觀察對(duì)象來(lái)攔截和短路上傳請(qǐng)求。

通過(guò)防抖來(lái)優(yōu)化與服務(wù)器的交互

如果你需要發(fā)一個(gè) HTTP 請(qǐng)求來(lái)響應(yīng)用戶的輸入,那么每次按鍵就發(fā)送一個(gè)請(qǐng)求的效率顯然不高。最好等用戶停止輸入后再發(fā)送請(qǐng)求。這種技術(shù)叫做防抖。

考慮下面這個(gè)模板,它讓用戶輸入一個(gè)搜索詞來(lái)按名字查找 npm 包。當(dāng)用戶在搜索框中輸入名字時(shí),?PackageSearchComponent ?就會(huì)把這個(gè)根據(jù)名字搜索包的請(qǐng)求發(fā)給 npm web API。

<input type="text" (keyup)="search(getValue($event))" id="name" placeholder="Search"/>

<ul>
  <li *ngFor="let package of packages$ | async">
    <b>{{package.name}} v.{{package.version}}</b> -
    <i>{{package.description}}</i>
  </li>
</ul>

在這里,?keyup ?事件綁定會(huì)將每個(gè)按鍵都發(fā)送到組件的 ?search()? 方法。

?$event.target? 的類(lèi)型在模板中只是 ?EventTarget?,而在 ?getValue()? 方法中,目標(biāo)會(huì)轉(zhuǎn)換成 ?HTMLInputElement ?類(lèi)型,以允許對(duì)它的 ?value ?屬性進(jìn)行類(lèi)型安全的訪問(wèn)。

getValue(event: Event): string {
  return (event.target as HTMLInputElement).value;
}

這里,?keyup ?事件綁定會(huì)把每次按鍵都發(fā)送給組件的 ?search()? 方法。下面的代碼片段使用 RxJS 的操作符為這個(gè)輸入實(shí)現(xiàn)了防抖。

withRefresh = false;
packages$!: Observable<NpmPackageInfo[]>;
private searchText$ = new Subject<string>();

search(packageName: string) {
  this.searchText$.next(packageName);
}

ngOnInit() {
  this.packages$ = this.searchText$.pipe(
    debounceTime(500),
    distinctUntilChanged(),
    switchMap(packageName =>
      this.searchService.search(packageName, this.withRefresh))
  );
}

constructor(private searchService: PackageSearchService) { }

?searchText$? 是來(lái)自用戶的搜索框值的序列。它被定義為 RxJS ?Subject ?類(lèi)型,這意味著它是一個(gè)多播 ?Observable?,它還可以通過(guò)調(diào)用 ?next(value)? 來(lái)自行發(fā)出值,就像在 ?search()? 方法中一樣。

除了把每個(gè) ?searchText ?的值都直接轉(zhuǎn)發(fā)給 ?PackageSearchService ?之外,?ngOnInit()? 中的代碼還通過(guò)下列三個(gè)操作符對(duì)這些搜索值進(jìn)行管道處理,以便只有當(dāng)它是一個(gè)新值并且用戶已經(jīng)停止輸入時(shí),要搜索的值才會(huì)抵達(dá)該服務(wù)。

RXJS 操作符

詳情

debounceTime(500)?

等待用戶停止輸入(本例中為 1/2 秒)。

distinctUntilChanged()

等待搜索文本發(fā)生變化。

switchMap()?

將搜索請(qǐng)求發(fā)送到服務(wù)。

這些代碼把 ?packages$? 設(shè)置成了使用搜索結(jié)果組合出的 ?Observable ?對(duì)象。模板中使用 ?AsyncPipe ?訂閱了 ?packages$?,一旦搜索結(jié)果的值發(fā)回來(lái)了,就顯示這些搜索結(jié)果。

使用 switchMap() 操作符

?switchMap()? 操作符接受一個(gè)返回 ?Observable ?的函數(shù)型參數(shù)。在這個(gè)例子中,?PackageSearchService.search? 像其它數(shù)據(jù)服務(wù)方法那樣返回一個(gè) ?Observable?。如果先前的搜索請(qǐng)求仍在進(jìn)行中(如網(wǎng)絡(luò)連接不良),它將取消該請(qǐng)求并發(fā)送新的請(qǐng)求。

注意:
?switchMap()? 會(huì)按照原始的請(qǐng)求順序返回這些服務(wù)的響應(yīng),而不用關(guān)心服務(wù)器實(shí)際上是以亂序返回的它們。

如果你覺(jué)得將來(lái)會(huì)復(fù)用這些防抖邏輯,可以把它移到單獨(dú)的工具函數(shù)中,或者移到 ?PackageSearchService ?中。

安全:XSRF 防護(hù)

跨站請(qǐng)求偽造 (XSRF 或 CSRF)是一個(gè)攻擊技術(shù),它能讓攻擊者假冒一個(gè)已認(rèn)證的用戶在你的網(wǎng)站上執(zhí)行未知的操作。?HttpClient ?支持一種通用的機(jī)制來(lái)防范 XSRF 攻擊。當(dāng)執(zhí)行 HTTP 請(qǐng)求時(shí),一個(gè)攔截器會(huì)從 cookie 中讀取 XSRF 令牌(默認(rèn)名字為 ?XSRF-TOKEN?),并且把它設(shè)置為一個(gè) HTTP 頭 ?X-XSRF-TOKEN?,由于只有運(yùn)行在你自己的域名下的代碼才能讀取這個(gè) cookie,因此后端可以確認(rèn)這個(gè) HTTP 請(qǐng)求真的來(lái)自你的客戶端應(yīng)用,而不是攻擊者。

默認(rèn)情況下,攔截器會(huì)在所有的修改型請(qǐng)求中(比如 POST 等)把這個(gè)請(qǐng)求頭發(fā)送給使用相對(duì) URL 的請(qǐng)求。但不會(huì)在 GET/HEAD 請(qǐng)求中發(fā)送,也不會(huì)發(fā)送給使用絕對(duì) URL 的請(qǐng)求。

要獲得這種優(yōu)點(diǎn),你的服務(wù)器需要在頁(yè)面加載或首個(gè) GET 請(qǐng)求中把一個(gè)名叫 ?XSRF-TOKEN? 的令牌寫(xiě)入可被 JavaScript 讀到的會(huì)話 cookie 中。而在后續(xù)的請(qǐng)求中,服務(wù)器可以驗(yàn)證這個(gè) cookie 是否與 HTTP 頭 ?X-XSRF-TOKEN? 的值一致,以確保只有運(yùn)行在你自己域名下的代碼才能發(fā)起這個(gè)請(qǐng)求。這個(gè)令牌必須對(duì)每個(gè)用戶都是唯一的,并且必須能被服務(wù)器驗(yàn)證,因此不能由客戶端自己生成令牌。把這個(gè)令牌設(shè)置為你的站點(diǎn)認(rèn)證信息并且加了鹽(salt)的摘要,以提升安全性。

為了防止多個(gè) Angular 應(yīng)用共享同一個(gè)域名或子域時(shí)出現(xiàn)沖突,要給每個(gè)應(yīng)用分配一個(gè)唯一的 cookie 名稱。

?HttpClient ?支持的只是 XSRF 防護(hù)方案的客戶端這一半。 你的后端服務(wù)必須配置為給頁(yè)面設(shè)置 cookie,并且要驗(yàn)證請(qǐng)求頭,以確保全都是合法的請(qǐng)求。如果不這么做,就會(huì)導(dǎo)致 Angular 的默認(rèn)防護(hù)措施失效。

配置自定義 cookie/header 名稱

如果你的后端服務(wù)中對(duì) XSRF 令牌的 cookie 或 頭使用了不一樣的名字,就要使用 ?HttpClientXsrfModule.withConfig()? 來(lái)覆蓋掉默認(rèn)值。

imports: [
  HttpClientModule,
  HttpClientXsrfModule.withOptions({
    cookieName: 'My-Xsrf-Cookie',
    headerName: 'My-Xsrf-Header',
  }),
],

測(cè)試 HTTP 請(qǐng)求

如同所有的外部依賴一樣,你必須把 HTTP 后端也 Mock 掉,以便你的測(cè)試可以模擬這種與后端的互動(dòng)。?@angular/common/http/testing? 庫(kù)能讓這種 Mock 工作變得直截了當(dāng)。

Angular 的 HTTP 測(cè)試庫(kù)是專為其中的測(cè)試模式而設(shè)計(jì)的。在這種模式下,會(huì)首先在應(yīng)用中執(zhí)行代碼并發(fā)起請(qǐng)求。然后,這個(gè)測(cè)試會(huì)期待發(fā)起或未發(fā)起過(guò)某個(gè)請(qǐng)求,并針對(duì)這些請(qǐng)求進(jìn)行斷言,最終對(duì)每個(gè)所預(yù)期的請(qǐng)求進(jìn)行刷新(flush)來(lái)對(duì)這些請(qǐng)求提供響應(yīng)。

最終,測(cè)試可能會(huì)驗(yàn)證這個(gè)應(yīng)用不曾發(fā)起過(guò)非預(yù)期的請(qǐng)求。

你可以到在線編程環(huán)境中運(yùn)行這些范例測(cè)試 / 下載范例。
本章所講的這些測(cè)試位于 ?src/testing/http-client.spec.ts? 中。在 ?src/app/heroes/heroes.service.spec.ts? 中還有一些測(cè)試,用于測(cè)試那些調(diào)用了 ?HttpClient ?的數(shù)據(jù)服務(wù)。

搭建測(cè)試環(huán)境

要開(kāi)始測(cè)試那些通過(guò) ?HttpClient ?發(fā)起的請(qǐng)求,就要導(dǎo)入 ?HttpClientTestingModule ?模塊,并把它加到你的 ?TestBed ?設(shè)置里去,代碼如下。

// Http testing module and mocking controller
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

// Other imports
import { TestBed } from '@angular/core/testing';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';

然后把 ?HTTPClientTestingModule ?添加到 ?TestBed ?中,并繼續(xù)設(shè)置被測(cè)服務(wù)。

describe('HttpClient testing', () => {
  let httpClient: HttpClient;
  let httpTestingController: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ HttpClientTestingModule ]
    });

    // Inject the http service and test controller for each test
    httpClient = TestBed.inject(HttpClient);
    httpTestingController = TestBed.inject(HttpTestingController);
  });
  /// Tests begin ///
});

現(xiàn)在,在測(cè)試中發(fā)起的這些請(qǐng)求會(huì)發(fā)給這些測(cè)試用的后端(testing backend),而不是標(biāo)準(zhǔn)的后端。

這種設(shè)置還會(huì)調(diào)用 ?TestBed.inject()?,來(lái)獲取注入的 ?HttpClient ?服務(wù)和模擬對(duì)象的控制器 ?HttpTestingController?,以便在測(cè)試期間引用它們。

期待并回復(fù)請(qǐng)求

現(xiàn)在,你就可以編寫(xiě)測(cè)試,等待 GET 請(qǐng)求并給出模擬響應(yīng)。

it('can test HttpClient.get', () => {
  const testData: Data = {name: 'Test Data'};

  // Make an HTTP GET request
  httpClient.get<Data>(testUrl)
    .subscribe(data =>
      // When observable resolves, result should match test data
      expect(data).toEqual(testData)
    );

  // The following `expectOne()` will match the request's URL.
  // If no requests or multiple requests matched that URL
  // `expectOne()` would throw.
  const req = httpTestingController.expectOne('/data');

  // Assert that the request is a GET.
  expect(req.request.method).toEqual('GET');

  // Respond with mock data, causing Observable to resolve.
  // Subscribe callback asserts that correct data was returned.
  req.flush(testData);

  // Finally, assert that there are no outstanding requests.
  httpTestingController.verify();
});

最后一步,驗(yàn)證沒(méi)有發(fā)起過(guò)預(yù)期之外的請(qǐng)求,足夠通用,因此你可以把它移到 ?afterEach()? 中:

afterEach(() => {
  // After every test, assert that there are no more pending requests.
  httpTestingController.verify();
});

自定義對(duì)請(qǐng)求的預(yù)期

如果僅根據(jù) URL 匹配還不夠,你還可以自行實(shí)現(xiàn)匹配函數(shù)。比如,你可以驗(yàn)證外發(fā)的請(qǐng)求是否帶有某個(gè)認(rèn)證頭:

// Expect one request with an authorization header
const req = httpTestingController.expectOne(
  request => request.headers.has('Authorization')
);

像前面的 ?expectOne()? 測(cè)試一樣,如果零或兩個(gè)以上的請(qǐng)求滿足了這個(gè)斷言,它就會(huì)拋出異常。

處理一個(gè)以上的請(qǐng)求

如果你需要在測(cè)試中對(duì)重復(fù)的請(qǐng)求進(jìn)行響應(yīng),可以使用 ?match()? API 來(lái)代替 ?expectOne()?,它的參數(shù)不變,但會(huì)返回一個(gè)與這些請(qǐng)求相匹配的數(shù)組。一旦返回,這些請(qǐng)求就會(huì)從將來(lái)要匹配的列表中移除,你要自己驗(yàn)證和刷新(flush)它。

// get all pending requests that match the given URL
const requests = httpTestingController.match(testUrl);
expect(requests.length).toEqual(3);

// Respond to each request with different results
requests[0].flush([]);
requests[1].flush([testData[0]]);
requests[2].flush(testData);

測(cè)試對(duì)錯(cuò)誤的預(yù)期

你還要測(cè)試應(yīng)用對(duì)于 HTTP 請(qǐng)求失敗時(shí)的防護(hù)。

調(diào)用 ?request.flush()? 并傳入一個(gè)錯(cuò)誤信息,如下所示。

it('can test for 404 error', () => {
  const emsg = 'deliberate 404 error';

  httpClient.get<Data[]>(testUrl).subscribe({
    next: () => fail('should have failed with the 404 error'),
    error: (error: HttpErrorResponse) => {
      expect(error.status).withContext('status').toEqual(404);
      expect(error.error).withContext('message').toEqual(emsg);
    },
  });

  const req = httpTestingController.expectOne(testUrl);

  // Respond with mock error
  req.flush(emsg, { status: 404, statusText: 'Not Found' });
});

另外,還可以用 ?ProgressEvent ?來(lái)調(diào)用 ?request.error()?。

it('can test for network error', done => {
  // Create mock ProgressEvent with type `error`, raised when something goes wrong
  // at network level. e.g. Connection timeout, DNS error, offline, etc.
  const mockError = new ProgressEvent('error');

  httpClient.get<Data[]>(testUrl).subscribe({
    next: () => fail('should have failed with the network error'),
    error: (error: HttpErrorResponse) => {
      expect(error.error).toBe(mockError);
      done();
    },
  });

  const req = httpTestingController.expectOne(testUrl);

  // Respond with mock error
  req.error(mockError);
});

將元數(shù)據(jù)傳遞給攔截器

許多攔截器都需要進(jìn)行配置或從配置中受益??紤]一個(gè)重試失敗請(qǐng)求的攔截器。默認(rèn)情況下,攔截器可能會(huì)重試請(qǐng)求三次,但是對(duì)于特別容易出錯(cuò)或敏感的請(qǐng)求,你可能要改寫(xiě)這個(gè)重試次數(shù)。

?HttpClient ?請(qǐng)求包含一個(gè)上下文,該上下文可以攜帶有關(guān)請(qǐng)求的元數(shù)據(jù)。該上下文可供攔截器讀取或修改,盡管發(fā)送請(qǐng)求時(shí)它并不會(huì)傳輸?shù)胶蠖朔?wù)器。這允許應(yīng)用程序或其他攔截器使用配置參數(shù)來(lái)標(biāo)記這些請(qǐng)求,比如重試請(qǐng)求的次數(shù)。

創(chuàng)建上下文令牌

?HttpContextToken ?用于在上下文中存儲(chǔ)和檢索值。你可以用 ?new ?運(yùn)算符創(chuàng)建上下文令牌,如以下例所示:

export const RETRY_COUNT = new HttpContextToken(() => 3);

?HttpContextToken ?創(chuàng)建期間傳遞的 lambda 函數(shù) ?() => 3? 有兩個(gè)用途:

  1. 它允許 TypeScript 推斷此令牌的類(lèi)型:?HttpContextToken<number>?。這個(gè)請(qǐng)求上下文是類(lèi)型安全的 —— 從請(qǐng)求上下文中讀取令牌將返回適當(dāng)類(lèi)型的值。
  2. 它會(huì)設(shè)置令牌的默認(rèn)值。如果尚未為此令牌設(shè)置其他值,那么這就是請(qǐng)求上下文返回的值。使用默認(rèn)值可以避免檢查是否已設(shè)置了特定值。

在發(fā)起請(qǐng)求時(shí)設(shè)置上下文值

發(fā)出請(qǐng)求時(shí),你可以提供一個(gè) ?HttpContext ?實(shí)例,在該實(shí)例中你已經(jīng)設(shè)置了一些上下文值。

this.httpClient
    .get('/data/feed', {
      context: new HttpContext().set(RETRY_COUNT, 5),
    })
    .subscribe(results => {/* ... */});

在攔截器中讀取上下文值

?HttpContext.get()? 在給定請(qǐng)求的上下文中讀取令牌的值。如果尚未顯式設(shè)置令牌的值,則 Angular 將返回令牌中指定的默認(rèn)值。

import {retry} from 'rxjs';

export class RetryInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const retryCount = req.context.get(RETRY_COUNT);

    return next.handle(req).pipe(
        // Retry the request a configurable number of times.
        retry(retryCount),
    );
  }
}

上下文是可變的(Mutable)

與 ?HttpRequest ?實(shí)例的大多數(shù)其他方面不同,請(qǐng)求上下文是可變的,并且在請(qǐng)求的其他不可變轉(zhuǎn)換過(guò)程中仍然存在。這允許攔截器通過(guò)此上下文協(xié)調(diào)來(lái)操作。比如,?RetryInterceptor ?示例可以使用第二個(gè)上下文令牌來(lái)跟蹤在執(zhí)行給定請(qǐng)求期間發(fā)生過(guò)多少錯(cuò)誤:

import {retry, tap} from 'rxjs/operators';
export const RETRY_COUNT = new HttpContextToken(() => 3);
export const ERROR_COUNT = new HttpContextToken(() => 0);

export class RetryInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const retryCount = req.context.get(RETRY_COUNT);

    return next.handle(req).pipe(
        tap({
              // An error has occurred, so increment this request's ERROR_COUNT.
             error: () => req.context.set(ERROR_COUNT, req.context.get(ERROR_COUNT) + 1)
            }),
        // Retry the request a configurable number of times.
        retry(retryCount),
    );
  }
}


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)