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ù)類。

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

  • 請(qǐng)求類型化響應(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)用類的依賴項(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ù)。返回的類型取決于你調(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"
}

要獲取這類數(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è)類型的響應(yīng)

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

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

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

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

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

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

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

修改后的組件方法,其回調(diào)函數(shù)中獲取一個(gè)帶類型的對(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)類型。比如,以下 ?subscribe ?回調(diào)會(huì)將 ?data ?作為對(duì)象接收,然后進(jìn)行類型轉(zhuǎn)換以訪問(wèn)屬性。

.subscribe(data => this.config = {
  heroesUrl: (data as any).heroesUrl,
  textfile:  (data as any).textfile,
});
OBSERVE 和 RESPONSE 的類型
?observe ?和 ?response ?選項(xiàng)的類型是字符串的聯(lián)合類型,而不是普通的字符串。
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 ?的類型推斷為 ?{responseType: string}?。該類型的 ?HttpClient.get? 太寬泛,無(wú)法傳給 ?HttpClient.get?,它希望 ?responseType ?的類型是特定的字符串之一。而 ?HttpClient ?就是以這種方式顯式輸入的,因此編譯器可以根據(jù)你提供的選項(xiàng)報(bào)告正確的返回類型。  

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

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 ?類型的 ?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è)帶有正確類型的 ?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)兩種類型的錯(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()? 一樣也有類型參數(shù),可以用它來(lái)指出你期望服務(wù)器返回特定類型的數(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 類型。

添加請(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 ?類的實(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 ?類和 ?params ?選項(xiàng)在你的 ?HttpRequest ?中添加 URL 查詢字符串。

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

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

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()? 方法的類。

這里是一個(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è)攔截器類,應(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 ?類本身就是一個(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),但 ?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)于此類傳輸?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 ?方法解釋了事件流中每種類型的 ?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? 的類型在模板中只是 ?EventTarget?,而在 ?getValue()? 方法中,目標(biāo)會(huì)轉(zhuǎn)換成 ?HTMLInputElement ?類型,以允許對(duì)它的 ?value ?屬性進(jìn)行類型安全的訪問(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 ?類型,這意味著它是一個(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 推斷此令牌的類型:?HttpContextToken<number>?。這個(gè)請(qǐng)求上下文是類型安全的 —— 從請(qǐng)求上下文中讀取令牌將返回適當(dāng)類型的值。
  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)