填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫Home

使用 HTTP 與後端服務進行通訊

Communicating with backend services using HTTP

大多數前端應用都要透過 HTTP 協議與伺服器通訊,才能下載或上傳資料並訪問其它後端服務。Angular 給應用提供了一個簡化的 HTTP 客戶端 API,也就是 @angular/common/http 中的 HttpClient 服務類別。

Most front-end applications need to communicate with a server over the HTTP protocol, in order to download or upload data and access other back-end services. Angular provides a simplified client HTTP API for Angular applications, the HttpClient service class in @angular/common/http.

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

The HTTP client service offers the following major features.

先決條件
Prerequisites

在使用 HTTPClientModule 之前,你應該對下列內容有基本的瞭解:

Before working with the HTTPClientModule, you should have a basic understanding of the following:

  • TypeScript 程式設計

    TypeScript programming

  • HTTP 協議的用法

    Usage of the HTTP protocol

  • Angular 的應用設計基礎,就像Angular 基本概念中描述的那樣

    Angular app-design fundamentals, as described in Angular Concepts

  • Observable 相關技術和運算子。參閱可觀察物件部分。

    Observable techniques and operators. See the Observables guide.

伺服器通訊的準備工作

Setup for server communication

要想使用 HttpClient,就要先匯入 Angular 的 HttpClientModule。大多數應用都會在根模組 AppModule 中匯入它。

Before you can use HttpClient, you need to import the Angular HttpClientModule. Most apps do so in the root AppModule.

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 {}
app/app.module.ts (excerpt)
      
      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 服務注入成一個應用類別的依賴項,如下面的 ConfigService 例子所示。

You can then inject the HttpClient service as a dependency of an application class, as shown in the following ConfigService example.

import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; @Injectable() export class ConfigService { constructor(private http: HttpClient) { } }
app/config/config.service.ts (excerpt)
      
      import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

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

HttpClient 服務為所有工作都使用了可觀察物件。你必須匯入範例程式碼片段中出現的 RxJS 可觀察物件和運算子。比如 ConfigService 中的這些匯入就很典型。

The HttpClient service makes use of observables for all transactions. You must import the RxJS observable and operator symbols that appear in the example snippets. These ConfigService imports are typical.

import { Observable, throwError } from 'rxjs'; import { catchError, retry } from 'rxjs/operators';
app/config/config.service.ts (RxJS imports)
      
      import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
    

你可以執行本指南附帶的現場演練 / 下載範例

You can run the現場演練 / 下載範例that accompanies this guide.

該範例應用不需要資料伺服器。它依賴於Angular in-memory-web-api,它替代了 HttpClient 模組中的 HttpBackend。這個替代服務會模擬 REST 式的後端的行為。

The sample app does not require a data server. It relies on the Angular in-memory-web-api, which replaces the HttpClient module's HttpBackend. The replacement service simulates the behavior of a REST-like backend.

看一下 AppModule 的這些匯入,看看它的配置方式。

Look at the AppModule imports to see how it is configured.

從伺服器請求資料

Requesting data from a server

使用 HTTPClient.get()方法從伺服器獲取資料。該非同步方法會發送一個 HTTP 請求,並返回一個 Observable,它會在收到響應時發出所請求到的資料。返回的型別取決於你呼叫時傳入的 observeresponseType 引數。

Use the HTTPClient.get()method to fetch data from a server. The asynchronous method sends an HTTP request, and returns an Observable that emits the requested data when the response is received. The return type varies based on the observe and responseType values that you pass to the call.

get() 方法有兩個引數。要獲取的端點 URL,以及一個可以用來配置請求的選項物件。

The get() method takes two arguments; the endpoint URL from which to fetch, and an options object that you can use to configure the request.

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

這些重要的選項包括 observeresponseType 屬性。

Important options include the observe and responseType properties.

  • observe 選項用於指定要返回的響應內容。

    The observe option specifies how much of the response to return.

  • responseType 選項指定返回資料的格式。

    The responseType option specifies the format in which to return data.

你可以使用 options 物件來配置傳出請求的各個方面。例如,在Adding headers 中,該服務使用 headers 選項屬性設定預設頭。

You can use the options object to configure various other aspects of an outgoing request. In Adding headers, for example, the service set the default headers using the headers option property.

使用 params 屬性可以配置帶HTTP URL 引數的請求,“ reportProgress 選項可以在傳輸大量資料時監聽進度事件

Use the params property to configure a request with HTTP URL parameters, and the reportProgress option to listen for progress events when transferring large amounts of data.

應用經常會從伺服器請求 JSON 資料。在 ConfigService 例子中,該應用需要伺服器 config.json 上的一個配置檔案來指定資源的 URL。

Applications often request JSON data from a server. In the ConfigService example, the app needs a configuration file on the server, config.json, that specifies resource URLs.

{ "heroesUrl": "api/heroes", "textfile": "assets/textfile.txt" }
assets/config.json
      
      {
  "heroesUrl": "api/heroes",
  "textfile": "assets/textfile.txt"
}
    

要獲取這類別資料,get() 呼叫需要以下幾個選項: {observe: 'body', responseType: 'json'}。這些是這些選項的預設值,所以下面的例子不會傳遞 options 物件。後面幾節展示了一些額外的選項。

To fetch this kind of data, the get() call needs the following options: {observe: 'body', responseType: 'json'}. These are the default values for those options, so the following examples do not pass the options object. Later sections show some of the additional option possibilities.

這個例子符合透過定義一個可複用的可注入服務來執行資料處理功能來建立可伸縮解決方案的最佳實踐。除了提取資料外,該服務還可以對資料進行後處理,新增錯誤處理,並新增重試邏輯。

The example conforms to the best practices for creating scalable solutions by defining a re-usable injectable service to perform the data-handling functionality. In addition to fetching data, the service can post-process the data, add error handling, and add retry logic.

ConfigService 使用 HttpClient.get() 方法獲取這個檔案。

The ConfigService fetches this file using the HttpClient.get() method.

configUrl = 'assets/config.json'; getConfig() { return this.http.get(this.configUrl); }
app/config/config.service.ts (getConfig v.1)
      
      configUrl = 'assets/config.json';

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

ConfigComponent 注入了 ConfigService 並呼叫了 getConfig 服務方法。

The ConfigComponent injects the ConfigService and calls the getConfig service method.

由於該服務方法返回了一個 Observable 配置資料,該元件會訂閱該方法的返回值。訂閱回呼(Callback)只會對後處理進行最少量的處理。它會把資料欄位複製到元件的 config 物件中,該物件在元件範本中是資料繫結的,用於顯示。

Because the service method returns an Observable of configuration data, the component subscribes to the method's return value. The subscription callback performs minimal post-processing. It copies the data fields into the component's config object, which is data-bound in the component template for display.

showConfig() { this.configService.getConfig() .subscribe((data: Config) => this.config = { heroesUrl: data.heroesUrl, textfile: data.textfile }); }
app/config/config.component.ts (showConfig v.1)
      
      showConfig() {
  this.configService.getConfig()
    .subscribe((data: Config) => this.config = {
        heroesUrl: data.heroesUrl,
        textfile:  data.textfile
    });
}
    

請求輸入一個型別的響應

Requesting a typed response

你可以構造自己的 HttpClient 請求來宣告響應物件的型別,以便讓輸出更容易、更明確。所指定的響應型別會在編譯時充當型別斷言。

You can structure your HttpClient request to declare the type of the response object, to make consuming the output easier and more obvious. Specifying the response type acts as a type assertion at compile time.

指定響應型別是在向 TypeScript 宣告,它應該把你的響應物件當做給定型別來使用。這是一種建構期檢查,它並不能保證伺服器會實際給出這種型別的響應物件。該伺服器需要自己確保返回伺服器 API 中指定的型別。

Specifying the response type is a declaration to TypeScript that it should treat your response as being of the given type. This is a build-time check and doesn't guarantee that the server will actually respond with an object of this type. It is up to the server to ensure that the type specified by the server API is returned.

要指定響應物件型別,首先要定義一個具有必需屬性的介面。這裡要使用介面而不是類別,因為響應物件是普通物件,無法自動轉換成類別的實例。

To specify the response object type, first define an interface with the required properties. Use an interface rather than a class, because the response is a plain object that cannot be automatically converted to an instance of a class.

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

接下來,在伺服器中把該介面指定為 HttpClient.get() 呼叫的型別引數。

Next, specify that interface as the HttpClient.get() call's type parameter in the service.

getConfig() { // now returns an Observable of Config return this.http.get<Config>(this.configUrl); }
app/config/config.service.ts (getConfig v.2)
      
      getConfig() {
  // now returns an Observable of Config
  return this.http.get<Config>(this.configUrl);
}
    

當把介面作為型別引數傳給 HttpClient.get() 方法時,你可以使用RxJS map 運算子來根據 UI 的需求轉換響應資料。然後,把轉換後的資料傳給非同步管道

When you pass an interface as a type parameter to the HttpClient.get() method, you can use the RxJS map operator to transform the response data as needed by the UI. You can then pass the transformed data to the async pipe.

修改後的元件方法,其回呼(Callback)函式中獲取一個帶型別的物件,它易於使用,且消費起來更安全:

The callback in the updated component method receives a typed data object, which is easier and safer to consume:

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

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

要訪問介面中定義的屬性,必須將從 JSON 獲得的普通物件顯式轉換為所需的響應型別。例如,以下 subscribe 回呼(Callback)會將 data 作為物件接收,然後進行型別轉換以訪問屬性。

To access properties that are defined in an interface, you must explicitly convert the plain object you get from the JSON to the required response type. For example, the following subscribe callback receives data as an Object, and then type-casts it in order to access the properties.

.subscribe(data => this.config = { heroesUrl: (data as any).heroesUrl, textfile: (data as any).textfile, });
      
      .subscribe(data => this.config = {
  heroesUrl: (data as any).heroesUrl,
  textfile:  (data as any).textfile,
});
    
*observe* 和 *response* 的型別
*observe* and *response* types

observeresponse 選項的型別是字串的聯合型別,而不是普通的字串。

The types of the observe and response options are string unions, rather than plain strings.

options: { ... observe?: 'body' | 'events' | 'response', ... responseType?: 'arraybuffer'|'blob'|'json'|'text', ... }
      
      options: {
    ...
    observe?: 'body' | 'events' | 'response',
    ...
    responseType?: 'arraybuffer'|'blob'|'json'|'text',
    ...
  }
    

這會引起混亂。例如:

This can cause confusion. For example:

// this works client.get('/foo', {responseType: 'text'}) // but this does NOT work const options = { responseType: 'text', }; client.get('/foo', options)
      
      // this works
client.get('/foo', {responseType: 'text'})

// but this does NOT work
const options = {
  responseType: 'text',
};
client.get('/foo', options)
    

在第二種情況下,TypeScript 會把 options 的型別推斷為 {responseType: string}。該型別的 HttpClient.get 太寬泛,無法傳給 HttpClient.get,它希望 responseType 的型別是特定的字串之一。而 HttpClient 就是以這種方式顯式輸入的,因此編譯器可以根據你提供的選項報告正確的返回型別。

In the second case, TypeScript infers the type of options to be {responseType: string}. The type is too wide to pass to HttpClient.get which is expecting the type of responseType to be one of the specific strings. HttpClient is typed explicitly this way so that the compiler can report the correct return type based on the options you provided.

使用 as const,可以讓 TypeScript 知道你並不是真的要使用字面字串型別:

Use as const to let TypeScript know that you really do mean to use a constant string type:

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

讀取完整的回應內文

Reading the full response

在前面的例子中,對 HttpClient.get() 的呼叫沒有指定任何選項。預設情況下,它返回了回應內文中包含的 JSON 資料。

In the previous example, the call to HttpClient.get() did not specify any options. By default, it returned the JSON data contained in the response body.

你可能還需要關於這次對話的更多資訊。比如,有時候伺服器會返回一個特殊的回應標頭或狀態碼,來指出某些在應用的工作流程中很重要的條件。

You might need more information about the transaction than is contained in the response body. Sometimes servers return special headers or status codes to indicate certain conditions that are important to the application workflow.

可以用 get() 方法的 observe 選項來告訴 HttpClient,你想要完整的響應物件:

Tell HttpClient that you want the full response with the observe option of the get() method:

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

現在,HttpClient.get() 會返回一個 HttpResponse 型別的 Observable,而不只是 JSON 資料。

Now HttpClient.get() returns an Observable of type HttpResponse rather than just the JSON data contained in the body.

該元件的 showConfigResponse() 方法會像顯示配置資料一樣顯示回應標頭:

The component's showConfigResponse() method displays the response headers as well as the configuration:

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 }; }); }
app/config/config.component.ts (showConfigResponse)
      
      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 };
    });
}
    

如你所見,該響應物件具有一個帶有正確型別的 body 屬性。

As you can see, the response object has a body property of the correct type.

發起 JSONP 請求

Making a JSONP request

當伺服器不支援 CORS 協議時,應用程式可以使用 HttpClient 跨域發出 JSONP 請求。

Apps can use the HttpClient to make JSONP requests across domains when a server doesn't support CORS protocol.

Angular 的 JSONP 請求會返回一個 Observable。 遵循訂閱可觀察物件變數的模式,並在使用 async 管道管理結果之前,使用 RxJS map 運算子轉換響應。

Angular JSONP requests return an Observable. Follow the pattern for subscribing to observables and use the RxJS map operator to transform the response before using the async pipe to manage the results.

在 Angular 中,透過在 NgModuleimports 中包含 HttpClientJsonpModule 來使用 JSONP。在以下範例中,searchHeroes() 方法使用 JSONP 請求來查詢名稱包含搜尋詞的英雄。

In Angular, use JSONP by including HttpClientJsonpModule in the NgModule imports. In the following example, the searchHeroes() method uses a JSONP request to query for heroes whose names contain the search term.

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

該請求將 heroesURL 作為第一個引數,並將回呼(Callback)函式名稱作為第二個引數。響應被包裝在回呼(Callback)函式中,該函式接受 JSONP 方法返回的可觀察物件,並將它們透過管道傳給錯誤處理程式。

This request passes the heroesURL as the first parameter and the callback function name as the second parameter. The response is wrapped in the callback function, which takes the observables returned by the JSONP method and pipes them through to the error handler.

請求非 JSON 資料

Requesting non-JSON data

不是所有的 API 都會返回 JSON 資料。在下面這個例子中,DownloaderService 中的方法會從伺服器讀取文字檔案, 並把檔案的內容記錄下來,然後把這些內容使用 Observable<string> 的形式返回給呼叫者。

Not all APIs return JSON data. In this next example, a DownloaderService method reads a text file from the server and logs the file contents, before returning those contents to the caller as an Observable<string>.

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 data => this.log(filename, data), error => this.logError(filename, error) ) ); }
app/downloader/downloader.service.ts (getTextFile)
      
      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
        data => this.log(filename, data),
        error => this.logError(filename, error)
      )
    );
}
    

這裡的 HttpClient.get() 返回字串而不是預設的 JSON 物件,因為它的 responseType 選項是 'text'

HttpClient.get() returns a string rather than the default JSON because of the responseType option.

RxJS 的 tap 運算子(如“竊聽”中所述)使程式碼可以檢查透過可觀察物件的成功值和錯誤值,而不會干擾它們。

The RxJS tap operator (as in "wiretap") lets the code inspect both success and error values passing through the observable without disturbing them.

DownloaderComponent 中的 download() 方法透過訂閱這個服務中的方法來發起一次請求。

A download() method in the DownloaderComponent initiates the request by subscribing to the service method.

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

處理請求錯誤

Handling request errors

如果請求在伺服器上失敗了,那麼 HttpClient 就會返回一個錯誤物件而不是一個成功的響應物件。

If the request fails on the server, HttpClient returns an error object instead of a successful response.

執行伺服器請求的同一個服務中也應該執行錯誤檢查、解釋和解析。

The same service that performs your server transactions should also perform error inspection, interpretation, and resolution.

發生錯誤時,你可以獲取失敗的詳細資訊,以便通知你的使用者。在某些情況下,你也可以自動重試該請求

When an error occurs, you can obtain details of what failed in order to inform your user. In some cases, you might also automatically retry the request.

獲取錯誤詳情

Getting error details

當資料訪問失敗時,應用會給使用者提供有用的反饋。原始的錯誤物件作為反饋並不是特別有用。除了檢測到錯誤已經發生之外,還需要獲取錯誤詳細資訊並使用這些細節來撰寫使用者友好的響應。

An app should give the user useful feedback when data access fails. A raw error object is not particularly useful as feedback. In addition to detecting that an error has occurred, you need to get error details and use those details to compose a user-friendly response.

可能會出現兩種型別的錯誤。

Two types of errors can occur.

  • 伺服器端可能會拒絕該請求,並返回狀態碼為 404 或 500 的 HTTP 響應。這些是錯誤響應

    The server backend might reject the request, returning an HTTP response with a status code such as 404 or 500. These are error responses.

  • 客戶端也可能出現問題,例如網路錯誤會讓請求無法成功完成,或者 RxJS 運算子也會丟擲異常。這些錯誤會產生 JavaScript 的 ErrorEvent 物件。

    Something could go wrong on the client-side such as a network error that prevents the request from completing successfully or an exception thrown in an RxJS operator. These errors produce JavaScript ErrorEvent objects.

HttpClient 在其 HttpErrorResponse 中會捕獲兩種錯誤。你可以檢查一下這個響應是否存在錯誤。

HttpClient captures both kinds of errors in its HttpErrorResponse. You can inspect that response to identify the error's cause.

下面的例子在之前定義的 ConfigService 中定義了一個錯誤處理程式。

The following example defines an error handler in the previously defined ConfigService.

private handleError(error: HttpErrorResponse) { if (error.error instanceof ErrorEvent) { // A client-side or network error occurred. Handle it accordingly. console.error('An error occurred:', error.error.message); } 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( 'Something bad happened; please try again later.'); }
app/config/config.service.ts (handleError)
      
      private handleError(error: HttpErrorResponse) {
  if (error.error instanceof ErrorEvent) {
    // A client-side or network error occurred. Handle it accordingly.
    console.error('An error occurred:', error.error.message);
  } 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(
    'Something bad happened; please try again later.');
}
    

該處理程式會返回一個帶有使用者友好的錯誤資訊的 RxJS ErrorObservable。下列程式碼修改了 getConfig() 方法,它使用一個管道HttpClient.get() 呼叫返回的所有 Observable 傳送給錯誤處理器。

The handler returns an RxJS ErrorObservable with a user-friendly error message. The following code updates the getConfig() method, using a pipe to send all observables returned by the HttpClient.get() call to the error handler.

getConfig() { return this.http.get<Config>(this.configUrl) .pipe( catchError(this.handleError) ); }
app/config/config.service.ts (getConfig v.3 with error handler)
      
      getConfig() {
  return this.http.get<Config>(this.configUrl)
    .pipe(
      catchError(this.handleError)
    );
}
    

重試失敗的請求

Retrying a failed request

有時候,錯誤只是臨時性的,只要重試就可能會自動消失。 比如,在移動端場景中可能會遇到網路中斷的情況,只要重試一下就能拿到正確的結果。

Sometimes the error is transient and goes away automatically if you try again. For example, network interruptions are common in mobile scenarios, and trying again can produce a successful result.

RxJS 函式庫提供了幾個重試運算子。例如,retry() 運算子會自動重新訂閱一個失敗的 Observable 幾次。重新訂閱 HttpClient 方法會導致它重新發出 HTTP 請求。

The RxJS library offers several retry operators. For example, the retry() operator automatically re-subscribes to a failed Observable a specified number of times. Re-subscribing to the result of an HttpClient method call has the effect of reissuing the HTTP request.

下面的例子示範瞭如何在把一個失敗的請求傳給錯誤處理程式之前,先透過管道傳給 retry() 運算子。

The following example shows how you can pipe a failed request to the retry() operator before passing it to the error handler.

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 ); }
app/config/config.service.ts (getConfig with 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
    );
}
    

把資料傳送到伺服器

Sending data to a server

除了從伺服器獲取資料外,HttpClient 還支援其它一些 HTTP 方法,比如 PUT,POST 和 DELETE,你可以用它們來修改遠端資料。

In addition to fetching data from a server, HttpClient supports other HTTP methods such as PUT, POST, and DELETE, which you can use to modify the remote data.

本指南中的這個範例應用包括一個簡化版本的《英雄之旅》,它會獲取英雄資料,並允許使用者新增、刪除和修改它們。 下面幾節在 HeroesService 範例中展示了資料更新方法的一些例子。

The sample app for this guide includes a simplified version of the "Tour of Heroes" example that fetches heroes and enables users to add, delete, and update them. The following sections show examples of the data-update methods from the sample's HeroesService.

發起一個 POST 請求

Making a POST request

應用經常在提交表單時透過 POST 請求向伺服器傳送資料。 下面這個例子中,HeroesService 在向資料庫新增英雄時發起了一個 HTTP POST 請求。

Apps often send data to a server with a POST request when submitting a form. In the following example, the HeroesService makes an HTTP POST request when adding a hero to the database.

/** 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)) ); }
app/heroes/heroes.service.ts (addHero)
      
      /** 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() 一樣也有型別引數,可以用它來指出你期望伺服器返回特定型別的資料。該方法需要一個資源 URL 和兩個額外的引數:

The HttpClient.post() method is similar to get() in that it has a type parameter, which you can use to specify that you expect the server to return data of a given type. The method takes a resource URL and two additional parameters:

  • body - 要在請求體中 POST 過去的資料。

    body - The data to POST in the body of the request.

  • options - 一個包含方法選項的物件,在這裡,它用來指定必要的請求頭

    options - An object containing method options which, in this case, specify required headers.

這個例子捕獲了前面所講的錯誤。

The example catches errors as described above.

HeroesComponent 透過訂閱該服務方法返回的 Observable 發起了一次實際的 POST 操作。

The HeroesComponent initiates the actual POST operation by subscribing to the Observable returned by this service method.

this.heroesService .addHero(newHero) .subscribe(hero => this.heroes.push(hero));
app/heroes/heroes.component.ts (addHero)
      
      this.heroesService
  .addHero(newHero)
  .subscribe(hero => this.heroes.push(hero));
    

當伺服器成功做出響應時,會帶有這個新建立的英雄,然後該元件就會把這個英雄新增到正在顯示的 heroes 列表中。

When the server responds successfully with the newly added hero, the component adds that hero to the displayed heroes list.

發起 DELETE 請求

Making a DELETE request

該應用可以把英雄的 id 傳給 HttpClient.delete 方法的請求 URL 來刪除一個英雄。

This application deletes a hero with the HttpClient.delete method by passing the hero's id in the request URL.

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

HeroesComponent 訂閱了該服務方法返回的 Observable 時,就會發起一次實際的 DELETE 操作。

The HeroesComponent initiates the actual DELETE operation by subscribing to the Observable returned by this service method.

this.heroesService .deleteHero(hero.id) .subscribe();
app/heroes/heroes.component.ts (deleteHero)
      
      this.heroesService
  .deleteHero(hero.id)
  .subscribe();
    

該元件不會等待刪除操作的結果,所以它的 subscribe (訂閱)中沒有回呼(Callback)函式。不過就算你不關心結果,也仍然要訂閱它。呼叫 subscribe() 方法會執行這個可觀察物件,這時才會真的發起 DELETE 請求。

The component isn't expecting a result from the delete operation, so it subscribes without a callback. Even though you are not using the result, you still have to subscribe. Calling the subscribe() method executes the observable, which is what initiates the DELETE request.

你必須呼叫 subscribe(),否則什麼都不會發生。僅僅呼叫 HeroesService.deleteHero() 是不會發起 DELETE 請求的。

You must call subscribe() or nothing happens. Just calling HeroesService.deleteHero() does not initiate the DELETE request.

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

別忘了訂閱

Always subscribe!

在呼叫方法返回的可觀察物件的 subscribe() 方法之前,HttpClient 方法不會發起 HTTP 請求。這適用於 HttpClient所有方法

An HttpClient method does not begin its HTTP request until you call subscribe() on the observable returned by that method. This is true for all HttpClient methods.

AsyncPipe會自動為你訂閱(以及取消訂閱)。

The AsyncPipesubscribes (and unsubscribes) for you automatically.

HttpClient 的所有方法返回的可觀察物件都設計為冷的。 HTTP 請求的執行都是延期執行的,讓你可以用 tapcatchError 這樣的運算子來在實際執行 HTTP 請求之前,先對這個可觀察物件進行擴充套件。

All observables returned from HttpClient methods are cold by design. Execution of the HTTP request is deferred, allowing you to extend the observable with additional operations such as tap and catchError before anything actually happens.

呼叫 subscribe(...) 會觸發這個可觀察物件的執行,並導致 HttpClient 組合並把 HTTP 請求發給伺服器。

Calling subscribe(...) triggers execution of the observable and causes HttpClient to compose and send the HTTP request to the server.

你可以把這些可觀察物件看做實際 HTTP 請求的藍圖

You can think of these observables as blueprints for actual HTTP requests.

實際上,每個 subscribe() 都會初始化此可觀察物件的一次單獨的、獨立的執行。 訂閱兩次就會導致發起兩個 HTTP 請求。

In fact, each subscribe() initiates a separate, independent execution of the observable. Subscribing twice results in two HTTP requests.

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

發起 PUT 請求

Making a PUT request

應用可以使用 HttpClient 服務傳送 PUT 請求。下面的 HeroesService 範例(就像 POST 範例一樣)用一個修改過的資料替換了該資源。

An app can send PUT requests using the HTTP client service. The following HeroesService example, like the POST example, replaces a resource with updated data.

/** 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)) ); }
app/heroes/heroes.service.ts (updateHero)
      
      /** 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))
    );
}
    

對於所有返回可觀察物件的 HTTP 方法,呼叫者(HeroesComponent.update()必須 subscribe()HttpClient.put() 返回的可觀察物件,才會真的發起請求。

As for any of the HTTP methods that return an observable, the caller, HeroesComponent.update() must subscribe()to the observable returned from the HttpClient.put() in order to initiate the request.

新增和更新請求頭

Adding and updating headers

很多伺服器都需要額外的頭來執行儲存操作。 例如,伺服器可能需要一個授權令牌,或者需要 Content-Type 頭來顯式宣告請求體的 MIME 型別。

Many servers require extra headers for save operations. For example, a server might require an authorization token, or "Content-Type" header to explicitly declare the MIME type of the request body.

新增請求頭
Adding headers

HeroesService 在一個 httpOptions 物件中定義了這樣的頭,它們被傳給每個 HttpClient 的儲存型方法。

The HeroesService defines such headers in an httpOptions object that are passed to every HttpClient save method.

import { HttpHeaders } from '@angular/common/http'; const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json', Authorization: 'my-auth-token' }) };
app/heroes/heroes.service.ts (httpOptions)
      
      import { HttpHeaders } from '@angular/common/http';

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

你不能直接修改前面的選項物件中的 HttpHeaders 請求頭,因為 HttpHeaders 類別的實例是不可變物件。請改用 set() 方法,以返回當前實例應用了新更改之後的副本。

You can't directly modify the existing headers within the previous options object because instances of the HttpHeaders class are immutable. Use the set() method instead, to return a clone of the current instance with the new changes applied.

下面的例子示範了當舊令牌過期時,可以在發起下一個請求之前更新授權頭。

The following example shows how, when an old token has expired, you can update the authorization header before making the next request.

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

配置 HTTP URL 引數

Configuring HTTP URL parameters

使用 HttpParams 類別和 params 選項在你的 HttpRequest 中新增 URL 查詢字串。

Use the HttpParams class with the params request option to add URL query strings in your HttpRequest.

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

The following example, the searchHeroes() method queries for heroes whose names contain the search term.

首先匯入 HttpParams 類別。

Start by importing HttpParams class.

import {HttpParams} from "@angular/common/http";
      
      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', [])) ); }
      
      /* 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', []))
    );
}
    

如果有搜尋詞,程式碼會用進行過 URL 編碼的搜尋引數來構造一個 options 物件。例如,如果搜尋詞是 "cat",那麼 GET 請求的 URL 就是 api/heroes?name=cat

If there is a search term, the code constructs an options object with an HTML URL-encoded search parameter. If the term is "cat", for example, the GET request URL would be api/heroes?name=cat.

HttpParams 是不可變物件。如果需要更新選項,請保留 .set() 方法的返回值。

The HttpParams object is immutable. If you need to update the options, save the returned value of the .set() method.

你也可以使用 fromString 變數從查詢字串中直接建立 HTTP 引數:

You can also create HTTP parameters directly from a query string by using the fromString variable:

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

攔截請求和響應

Intercepting requests and responses

藉助攔截機制,你可以宣告一些攔截器,它們可以檢查並轉換從應用中發給伺服器的 HTTP 請求。這些攔截器還可以在返回應用的途中檢查和轉換來自伺服器的響應。多個攔截器構成了請求/響應處理器的雙向連結串列。

With interception, you declare interceptors that inspect and transform HTTP requests from your application to a server. The same interceptors can also inspect and transform a server's responses on their way back to the application. Multiple interceptors form a forward-and-backward chain of request/response handlers.

攔截器可以用一種常規的、標準的方式對每一次 HTTP 的請求/響應任務執行從認證到記日誌等很多種隱式任務。

Interceptors can perform a variety of implicit tasks, from authentication to logging, in a routine, standard way, for every HTTP request/response.

如果沒有攔截機制,那麼開發人員將不得不對每次 HttpClient 呼叫顯式實現這些任務。

Without interception, developers would have to implement these tasks explicitly for each HttpClient method call.

編寫攔截器

Write an interceptor

要實現攔截器,就要實現一個實現了 HttpInterceptor 介面中的 intercept() 方法的類別。

To implement an interceptor, declare a class that implements the intercept() method of the HttpInterceptor interface.

這裡是一個什麼也不做的空白攔截器,它只會不做任何修改的傳遞這個請求。

Here is a do-nothing noop interceptor that simply passes the request through without touching it:

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); } }
app/http-interceptors/noop-interceptor.ts
      
      import { Injectable } from '@angular/core';
import {
  HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';

import { Observable } from 'rxjs';

/** Pass untouched request through to the next request handler. */
@Injectable()
export class NoopInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {
    return next.handle(req);
  }
}
    

intercept 方法會把請求轉換成一個最終返回 HTTP 回應內文的 Observable。 在這個場景中,每個攔截器都完全能自己處理這個請求。

The intercept method transforms a request into an Observable that eventually returns the HTTP response. In this sense, each interceptor is fully capable of handling the request entirely by itself.

大多數攔截器攔截都會在傳入時檢查請求,然後把(可能被修改過的)請求轉發給 next 物件的 handle() 方法,而 next 物件實現了 HttpHandler介面。

Most interceptors inspect the request on the way in and forward the (perhaps altered) request to the handle() method of the next object which implements the HttpHandlerinterface.

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

intercept() 一樣,handle() 方法也會把 HTTP 請求轉換成 HttpEvents組成的 Observable,它最終包含的是來自伺服器的響應。 intercept() 函式可以檢查這個可觀察物件,並在把它返回給呼叫者之前修改它。

Like intercept(), the handle() method transforms an HTTP request into an Observable of HttpEventswhich ultimately include the server's response. The intercept() method could inspect that observable and alter it before returning it to the caller.

這個無操作的攔截器,會直接使用原始的請求呼叫 next.handle(),並返回它返回的可觀察物件,而不做任何後續處理。

This no-op interceptor simply calls next.handle() with the original request and returns the observable without doing a thing.

next 物件

The next object

next 物件表示攔截器連結串列中的下一個攔截器。 這個連結串列中的最後一個 next 物件就是 HttpClient 的後端處理器(backend handler),它會把請求發給伺服器,並接收伺服器的響應。

The next object represents the next interceptor in the chain of interceptors. The final next in the chain is the HttpClient backend handler that sends the request to the server and receives the server's response.

大多數的攔截器都會呼叫 next.handle(),以便這個請求流能走到下一個攔截器,並最終傳給後端處理器。 攔截器也可以不呼叫 next.handle(),使這個鏈路短路,並返回一個帶有人工構造出來的伺服器響應的 自己的 Observable

Most interceptors call next.handle() so that the request flows through to the next interceptor and, eventually, the backend handler. An interceptor could skip calling next.handle(), short-circuit the chain, and return its own Observablewith an artificial server response.

這是一種常見的中介軟體模式,在像 Express.js 這樣的框架中也會找到它。

This is a common middleware pattern found in frameworks such as Express.js.

提供這個攔截器

Provide the interceptor

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

The NoopInterceptor is a service managed by Angular's dependency injection (DI) system. Like other services, you must provide the interceptor class before the app can use it.

由於攔截器是 HttpClient 服務的(可選)依賴,所以你必須在提供 HttpClient 的同一個(或其各級父注入器)注入器中提供這些攔截器。 那些在 DI 建立完 HttpClient 之後再提供的攔截器將會被忽略。

Because interceptors are (optional) dependencies of the HttpClient service, you must provide them in the same injector (or a parent of the injector) that provides HttpClient. Interceptors provided after DI creates the HttpClient are ignored.

由於在 AppModule 中匯入了 HttpClientModule,導致本應用在其根注入器中提供了 HttpClient。所以你也同樣要在 AppModule 中提供這些攔截器。

This app provides HttpClient in the app's root injector, as a side-effect of importing the HttpClientModule in AppModule. You should provide interceptors in AppModule as well.

在從 @angular/common/http 中匯入了 HTTP_INTERCEPTORS 注入令牌之後,編寫如下的 NoopInterceptor 提供者註冊語句:

After importing the HTTP_INTERCEPTORS injection token from @angular/common/http, write the NoopInterceptor provider like this:

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

注意 multi: true 選項。 這個必須的選項會告訴 Angular HTTP_INTERCEPTORS 是一個多重提供者的令牌,表示它會注入一個多值的陣列,而不是單一的值。

Note the multi: true option. This required setting tells Angular that HTTP_INTERCEPTORS is a token for a multiprovider that injects an array of values, rather than a single value.

也可以直接把這個提供者新增到 AppModule 中的提供者陣列中,不過那樣會非常囉嗦。況且,你將來還會用這種方式建立更多的攔截器並提供它們。 你還要特別注意提供這些攔截器的順序

You could add this provider directly to the providers array of the AppModule. However, it's rather verbose and there's a good chance that you'll create more interceptors and provide them in the same way. You must also pay close attention to the order in which you provide these interceptors.

認真考慮建立一個封裝桶(barrel)檔案,用於把所有攔截器都收集起來,一起提供給 httpInterceptorProviders 陣列,可以先從這個 NoopInterceptor 開始。

Consider creating a "barrel" file that gathers all the interceptor providers into an httpInterceptorProviders array, starting with this first one, the NoopInterceptor.

/* "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 }, ];
app/http-interceptors/index.ts
      
      /* "Barrel" of Http Interceptors */
import { HTTP_INTERCEPTORS } from '@angular/common/http';

import { NoopInterceptor } from './noop-interceptor';

/** Http interceptor providers in outside-in order */
export const httpInterceptorProviders = [
  { provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
];
    

然後匯入它,並把它加到 AppModuleproviders 陣列中,就像這樣:

Then import and add it to the AppModule providers array like this:

providers: [ httpInterceptorProviders ],
app/app.module.ts (interceptor providers)
      
      providers: [
  httpInterceptorProviders
],
    

當你再建立新的攔截器時,就同樣把它們新增到 httpInterceptorProviders 陣列中,而不用再修改 AppModule

As you create new interceptors, add them to the httpInterceptorProviders array and you won't have to revisit the AppModule.

在完整版的範例程式碼中還有更多的攔截器。

There are many more interceptors in the complete sample code.

攔截器的順序

Interceptor order

Angular 會按照你提供它們的順序應用這些攔截器。 如果你提供攔截器的順序是先 A,再 B,再 C,那麼請求階段的執行順序就是 A->B->C,而響應階段的執行順序則是 C->B->A

Angular applies interceptors in the order that you provide them. If you provide interceptors A, then B, then C, requests will flow in A->B->C and responses will flow out C->B->A.

以後你就再也不能修改這些順序或移除某些攔截器了。 如果你需要動態啟用或禁用某個攔截器,那就要在那個攔截器中自行實現這個功能。

You cannot change the order or remove interceptors later. If you need to enable and disable an interceptor dynamically, you'll have to build that capability into the interceptor itself.

處理攔截器事件

Handling interceptor events

大多數 HttpClient 方法都會返回 HttpResponse<any> 型的可觀察物件。HttpResponse 類別本身就是一個事件,它的型別是 HttpEventType.Response。但是,單個 HTTP 請求可以產生其它型別的多個事件,包括報告上傳和下載進度的事件。HttpInterceptor.intercept()HttpHandler.handle() 會返回 HttpEvent<any> 型的可觀察物件。

Most HttpClient methods return observables of HttpResponse<any>. The HttpResponse class itself is actually an event, whose type is HttpEventType.Response. A single HTTP request can, however, generate multiple events of other types, including upload and download progress events. The methods HttpInterceptor.intercept() and HttpHandler.handle() return observables of HttpEvent<any>.

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

Many interceptors are only concerned with the outgoing request and return the event stream from next.handle() without modifying it. Some interceptors, however, need to examine and modify the response from next.handle(); these operations can see all of these events in the stream.

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

Although interceptors are capable of modifying requests and responses, the HttpRequest and HttpResponse instance properties are readonly, rendering them largely immutable.

有充足的理由把它們做成不可變物件:應用可能會重試傳送很多次請求之後才能成功,這就意味著這個攔截器連結串列可能會多次重複處理同一個請求。 如果攔截器可以修改原始的請求物件,那麼重試階段的操作就會從修改過的請求開始,而不是原始請求。 而這種不可變性,可以確保這些攔截器在每次重試時看到的都是同樣的原始請求。

They are immutable for a good reason: an app might retry a request several times before it succeeds, which means that the interceptor chain can re-process the same request multiple times. If an interceptor could modify the original request object, the re-tried operation would start from the modified request rather than the original. Immutability ensures that interceptors see the same request for each try.

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

Your interceptor should return every event without modification unless it has a compelling reason to do otherwise.

TypeScript 會阻止你設定 HttpRequest 的唯讀屬性。

TypeScript prevents you from setting HttpRequest read-only properties.

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

如果你必須修改一個請求,先把它複製一份,修改這個複製體後再把它傳給 next.handle()。你可以在一步中複製並修改此請求,例子如下。

If you must alter a request, clone it first and modify the clone before passing it to next.handle(). You can clone and modify the request in a single step, as shown in the following example.

// 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);
app/http-interceptors/ensure-https-interceptor.ts (excerpt)
      
      // clone request and replace 'http://' with 'https://' at the same time
const secureReq = req.clone({
  url: req.url.replace('http://', 'https://')
});
// send the cloned, "secure" request to the next handler.
return next.handle(secureReq);
    

這個 clone() 方法的雜湊型引數允許你在複製出複製體的同時改變該請求的某些特定屬性。

The clone() method's hash argument allows you to mutate specific properties of the request while copying the others.

修改請求體

Modifying a request body

readonly 這種賦值保護,無法防範深修改(修改子物件的屬性),也不能防範你修改請求體物件中的屬性。

The readonly assignment guard can't prevent deep updates and, in particular, it can't prevent you from modifying a property of a request body object.

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

如果必須修改請求體,請執行以下步驟。

If you must modify the request body, follow these steps.

  1. 複製請求體並在副本中進行修改。

    Copy the body and make your change in the copy.

  2. 使用 clone() 方法複製這個請求物件。

    Clone the request object, using its clone() method.

  3. 用修改過的副本替換被複製的請求體。

    Replace the clone's body with the modified copy.

// 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);
app/http-interceptors/trim-name-interceptor.ts (excerpt)
      
      // 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);
    

複製時清除請求體

Clearing the request body in a clone

有時,你需要清除請求體而不是替換它。為此,請將複製後的請求體設定為 null

Sometimes you need to clear the request body rather than replace it. To do this, set the cloned request body to null.

提示:如果你把複製後的請求體設為 undefined,那麼 Angular 會認為你想讓請求體保持原樣。

Tip: If you set the cloned request body to undefined, Angular assumes you intend to leave the body as is.

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

設定預設請求頭

Setting default headers

應用通常會使用攔截器來設定外發請求的預設請求頭。

Apps often use an interceptor to set default headers on outgoing requests.

該範例應用具有一個 AuthService,它會產生一個認證令牌。 在這裡,AuthInterceptor 會注入該服務以獲取令牌,並對每一個外發的請求新增一個帶有該令牌的認證頭:

The sample app has an AuthService that produces an authorization token. Here is its AuthInterceptor that injects that service to get the token and adds an authorization header with that token to every outgoing request:

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); } }
app/http-interceptors/auth-interceptor.ts
      
      import { AuthService } from '../auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private auth: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    // Get the auth token from the service.
    const authToken = this.auth.getAuthorizationToken();

    // Clone the request and replace the original headers with
    // cloned headers, updated with the authorization.
    const authReq = req.clone({
      headers: req.headers.set('Authorization', authToken)
    });

    // send cloned request with header to the next handler.
    return next.handle(authReq);
  }
}
    

這種在複製請求的同時設定新請求頭的操作太常見了,因此它還有一個快捷方式 setHeaders

The practice of cloning a request to set new headers is so common that there's a setHeaders shortcut for it:

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

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

An interceptor that alters headers can be used for a number of different operations, including:

  • 認證 / 授權

    Authentication/authorization

  • 控制快取行為。比如 If-Modified-Since

    Caching behavior; for example, If-Modified-Since

  • XSRF 防護

    XSRF protection

用攔截器記日誌

Using interceptors for logging

因為攔截器可以同時處理請求和響應,所以它們也可以對整個 HTTP 操作執行計時和記錄日誌等任務。

Because interceptors can process the request and response together, they can perform tasks such as timing and logging an entire HTTP operation.

考慮下面這個 LoggingInterceptor,它捕獲請求的發起時間、響應的接收時間,並使用注入的 MessageService 來發送總共花費的時間。

Consider the following LoggingInterceptor, which captures the time of the request, the time of the response, and logs the outcome with the elapsed time with the injected MessageService.

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); }) ); } }
app/http-interceptors/logging-interceptor.ts)
      
      import { finalize, tap } from 'rxjs/operators';
import { MessageService } from '../message.service';

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
  constructor(private messenger: MessageService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const started = Date.now();
    let ok: string;

    // extend server response observable with logging
    return next.handle(req)
      .pipe(
        tap(
          // Succeeds when there is a response; ignore other events
          event => ok = event instanceof HttpResponse ? 'succeeded' : '',
          // Operation failed; error is an HttpErrorResponse
          error => ok = 'failed'
        ),
        // Log when response observable either completes or errors
        finalize(() => {
          const elapsed = Date.now() - started;
          const msg = `${req.method} "${req.urlWithParams}"
             ${ok} in ${elapsed} ms.`;
          this.messenger.add(msg);
        })
      );
  }
}
    

RxJS 的 tap 運算子會捕獲請求成功了還是失敗了。 RxJS 的 finalize 運算子無論在響應成功還是失敗時都會呼叫(這是必須的),然後把結果彙報給 MessageService

The RxJS tap operator captures whether the request succeeded or failed. The RxJS finalize operator is called when the response observable either errors or completes (which it must), and reports the outcome to the MessageService.

在這個可觀察物件的流中,無論是 tap 還是 finalize 接觸過的值,都會照常傳送給呼叫者。

Neither tap nor finalize touch the values of the observable stream returned to the caller.

用攔截器實現快取

Using interceptors for caching

攔截器還可以自行處理這些請求,而不用轉發給 next.handle()

Interceptors can handle requests by themselves, without forwarding to next.handle().

比如,你可能會想快取某些請求和響應,以便提升效能。 你可以把這種快取操作委託給某個攔截器,而不破壞你現有的各個資料服務。

For example, you might decide to cache certain requests and responses to improve performance. You can delegate caching to an interceptor without disturbing your existing data services.

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

The CachingInterceptor in the following example demonstrates this approach.

@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); } }
app/http-interceptors/caching-interceptor.ts)
      
      @Injectable()
export class CachingInterceptor implements HttpInterceptor {
  constructor(private cache: RequestCache) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    // continue if not cacheable.
    if (!isCacheable(req)) { return next.handle(req); }

    const cachedResponse = this.cache.get(req);
    return cachedResponse ?
      of(cachedResponse) : sendRequest(req, next, this.cache);
  }
}
    
  • isCacheable() 函式用於決定該請求是否允許快取。 在這個例子中,只有發到 npm 套件搜尋 API 的 GET 請求才是可以快取的。

    The isCacheable() function determines if the request is cacheable. In this sample, only GET requests to the npm package search api are cacheable.

  • 如果該請求是不可快取的,該攔截器只會把該請求轉發給連結串列中的下一個處理器。

    If the request is not cacheable, the interceptor simply forwards the request to the next handler in the chain.

  • 如果可快取的請求在快取中找到了,該攔截器就會透過 of() 函式返回一個已快取的回應內文的可觀察物件,然後繞過 next 處理器(以及所有其它下游攔截器)。

    If a cacheable request is found in the cache, the interceptor returns an of() observable with the cached response, by-passing the next handler (and all other interceptors downstream).

  • 如果可快取的請求不在快取中,程式碼會呼叫 sendRequest()。這個函式會建立一個沒有請求頭的請求複製體,這是因為 npm API 禁止它們。然後,該函式把請求的複製體轉發給 next.handle(),它會最終呼叫伺服器並返回來自伺服器的響應物件。

    If a cacheable request is not in cache, the code calls sendRequest(). This function creates a request clone without headers, because the npm API forbids them. The function then forwards the clone of the request to next.handle() which ultimately calls the server and returns the server's response.

/** * 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. } }) ); }
      
      /**
 * Get server response observable by sending request to `next()`.
 * Will add the response to the cache on the way out.
 */
function sendRequest(
  req: HttpRequest<any>,
  next: HttpHandler,
  cache: RequestCache): Observable<HttpEvent<any>> {

  // No headers allowed in npm search request
  const noHeaderReq = req.clone({ headers: new HttpHeaders() });

  return next.handle(noHeaderReq).pipe(
    tap(event => {
      // There may be other events besides the response.
      if (event instanceof HttpResponse) {
        cache.put(req, event); // Update the cache.
      }
    })
  );
}
    

注意 sendRequest() 是如何在返回應用程式的過程中攔截響應的。該方法透過 tap() 運算子來管理響應物件,該運算子的回呼(Callback)函式會把該響應物件新增到快取中。

Note how sendRequest() intercepts the response on its way back to the application. This method pipes the response through the tap() operator, whose callback adds the response to the cache.

然後,原始的響應會透過這些攔截器鏈,原封不動的回到伺服器的呼叫者那裡。

The original response continues untouched back up through the chain of interceptors to the application caller.

資料服務,比如 PackageSearchService,並不知道它們收到的某些 HttpClient 請求實際上是從快取的請求中返回來的。

Data services, such as PackageSearchService, are unaware that some of their HttpClient requests actually return cached responses.

用攔截器來請求多個值

Using interceptors to request multiple values

HttpClient.get() 方法通常會返回一個可觀察物件,它會發出一個值(資料或錯誤)。攔截器可以把它改成一個可以發出多個值的可觀察物件。

The HttpClient.get() method normally returns an observable that emits a single value, either the data or an error. An interceptor can change this to an observable that emits multiple values.

修改後的 CachingInterceptor 版本可以返回一個立即發出所快取響應的可觀察物件,然後把請求傳送到 NPM 的 Web API,然後把修改過的搜尋結果重新發出一次。

The following revised version of the CachingInterceptor optionally returns an observable that immediately emits the cached response, sends the request on to the npm web API, and emits again later with the updated search results.

// 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
if (req.headers.get('x-refresh')) {
  const results$ = sendRequest(req, next, this.cache);
  return cachedResponse ?
    results$.pipe( startWith(cachedResponse) ) :
    results$;
}
// cache-or-fetch
return cachedResponse ?
  of(cachedResponse) : sendRequest(req, next, this.cache);
    

cache-then-refresh 選項是由一個自訂的 x-refresh 請求頭觸發的。

The cache-then-refresh option is triggered by the presence of a custom x-refresh header.

PackageSearchComponent 中的一個檢查框會切換 withRefresh 標識, 它是 PackageSearchService.search() 的引數之一。 search() 方法建立了自訂的 x-refresh 頭,並在呼叫 HttpClient.get() 前把它新增到請求裡。

A checkbox on the PackageSearchComponent toggles a withRefresh flag, which is one of the arguments to PackageSearchService.search(). That search() method creates the custom x-refresh header and adds it to the request before calling HttpClient.get().

修改後的 CachingInterceptor 會發起一個伺服器請求,而不管有沒有快取的值。 就像 前面sendRequest() 方法一樣進行訂閱。 在訂閱 results$ 可觀察物件時,就會發起這個請求。

The revised CachingInterceptor sets up a server request whether there's a cached value or not, using the same sendRequest() method described above. The results$ observable makes the request when subscribed.

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

    If there's no cached value, the interceptor returns results$.

  • 如果有快取的值,這些程式碼就會把快取的響應加入到 result$管道中,使用重組後的可觀察物件進行處理,併發出兩次。 先立即發出一次快取的回應內文,然後發出來自伺服器的響應。 訂閱者將會看到一個包含這兩個響應的序列。

    If there is a cached value, the code pipes the cached response onto results$, producing a recomposed observable that emits twice, the cached response first (and immediately), followed later by the response from the server. Subscribers see a sequence of two responses.

追蹤和顯示請求進度

Tracking and showing request progress

應用程式有時會傳輸大量資料,而這些傳輸可能要花很長時間。檔案上傳就是典型的例子。你可以透過提供關於此類別傳輸的進度反饋,為使用者提供更好的體驗。

Sometimes applications transfer large amounts of data and those transfers can take a long time. File uploads are a typical example. You can give the users a better experience by providing feedback on the progress of such transfers.

要想發出一個帶有進度事件的請求,你可以建立一個 HttpRequest 實例,並把 reportProgress 選項設定為 true 來啟用對進度事件的追蹤。

To make a request with progress events enabled, you can create an instance of HttpRequest with the reportProgress option set true to enable tracking of progress events.

const req = new HttpRequest('POST', '/upload/file', file, { reportProgress: true });
app/uploader/uploader.service.ts (upload request)
      
      const req = new HttpRequest('POST', '/upload/file', file, {
  reportProgress: true
});
    

提示:每個進度事件都會觸發變更檢測,所以只有當需要在 UI 上報告進度時,你才應該開啟它們。

Tip: Every progress event triggers change detection, so only turn them on if you need to report progress in the UI.

HttpClient.request()和 HTTP 方法一起使用時,可以用 observe: 'events'來檢視所有事件,包括傳輸的進度。

When using HttpClient.request()with an HTTP method, configure the method with observe: 'events'to see all events, including the progress of transfers.

接下來,把這個請求物件傳給 HttpClient.request() 方法,該方法返回一個 HttpEventsObservable(與 攔截器 部分處理過的事件相同)。

Next, pass this request object to the HttpClient.request() method, which returns an Observable of HttpEvents (the same events processed by interceptors).

// 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)) );
app/uploader/uploader.service.ts (upload body)
      
      // 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

The getEventMessage method interprets each type of HttpEvent in the event stream.

/** 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 = Math.round(100 * event.loaded / event.total); 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}.`; } }
app/uploader/uploader.service.ts (getEventMessage)
      
      /** 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 = Math.round(100 * event.loaded / event.total);
      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}.`;
  }
}
    

本指南中的範例應用中沒有用來接受上傳檔案的伺服器。app/http-interceptors/upload-interceptor.tsUploadInterceptor 透過返回一個模擬這些事件的可觀察物件來攔截和短路上傳請求。

The sample app for this guide doesn't have a server that accepts uploaded files. The UploadInterceptor in app/http-interceptors/upload-interceptor.ts intercepts and short-circuits upload requests by returning an observable of simulated events.

透過防抖來優化與伺服器的互動

Optimizing server interaction with debouncing

如果你需要發一個 HTTP 請求來響應使用者的輸入,那麼每次擊鍵就傳送一個請求的效率顯然不高。最好等使用者停止輸入後再發送請求。這種技術叫做防抖。

If you need to make an HTTP request in response to user input, it's not efficient to send a request for every keystroke. It's better to wait until the user stops typing and then send a request. This technique is known as debouncing.

考慮下面這個範本,它讓使用者輸入一個搜尋詞來按名字查詢 npm 套件。 當用戶在搜尋框中輸入名字時,PackageSearchComponent 就會把這個根據名字搜尋套件的請求發給 npm web API。

Consider the following template, which lets a user enter a search term to find an npm package by name. When the user enters a name in a search-box, the PackageSearchComponent sends a search request for a package with that name to the npm web API.

<input (keyup)="search($event.target.value)" 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>
app/package-search/package-search.component.html (search)
      
      <input (keyup)="search($event.target.value)" 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 事件繫結會把每次擊鍵都發送給元件的 search() 方法。下面的程式碼片段使用 RxJS 的運算子為這個輸入實現了防抖。

Here, the keyup event binding sends every keystroke to the component's search() method. The following snippet implements debouncing for this input using RxJS operators.

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) { }
app/package-search/package-search.component.ts (excerpt)
      
      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$ 是來自使用者的搜尋框值的序列。它被定義為 RxJS Subject 型別,這意味著它是一個多播 Observable,它還可以透過呼叫 next(value) 來自行發出值,就像在 search() 方法中一樣。

The searchText$ is the sequence of search-box values coming from the user. It's defined as an RxJS Subject, which means it is a multicasting Observable that can also emit values for itself by calling next(value), as happens in the search() method.

除了把每個 searchText 的值都直接轉發給 PackageSearchService 之外,ngOnInit() 中的程式碼還透過下列三個運算子對這些搜尋值進行管道處理,以便只有當它是一個新值並且使用者已經停止輸入時,要搜尋的值才會抵達該服務。

Rather than forward every searchText value directly to the injected PackageSearchService, the code in ngOnInit() pipes search values through three operators, so that a search value reaches the service only if it's a new value and the user has stopped typing.

  • debounceTime(500)⁠—等待使用者停止輸入(本例中為 1/2 秒)。

    debounceTime(500)⁠—Wait for the user to stop typing (1/2 second in this case).

  • distinctUntilChanged()⁠—等待搜尋文字發生變化。

    distinctUntilChanged()⁠—Wait until the search text changes.

  • switchMap()⁠—將搜尋請求傳送到服務。

    switchMap()⁠—Send the search request to the service.

這些程式碼把 packages$ 設定成了使用搜索結果組合出的 Observable 物件。 範本中使用 AsyncPipe 訂閱了 packages$,一旦搜尋結果的值發回來了,就顯示這些搜尋結果。

The code sets packages$ to this re-composed Observable of search results. The template subscribes to packages$ with the AsyncPipe and displays search results as they arrive.

關於 withRefresh 選項的更多資訊,請參閱使用攔截器來請求多個值

See Using interceptors to request multiple values for more about the withRefresh option.

使用 switchMap() 運算子

Using the switchMap() operator

switchMap() 運算子接受一個返回 Observable 的函式型引數。在這個例子中,PackageSearchService.search 像其它資料服務方法那樣返回一個 Observable。如果先前的搜尋請求仍在進行中 (如網路連線不良),它將取消該請求併發送新的請求。

The switchMap() operator takes a function argument that returns an Observable. In the example, PackageSearchService.search returns an Observable, as other data service methods do. If a previous search request is still in-flight (as when the network connection is poor), the operator cancels that request and sends a new one.

請注意,switchMap() 會按照原始的請求順序返回這些服務的響應,而不用關心伺服器實際上是以亂序返回的它們。

Note that switchMap() returns service responses in their original request order, even if the server returns them out of order.

如果你覺得將來會複用這些防抖邏輯, 可以把它移到單獨的工具函式中,或者移到 PackageSearchService 中。

If you think you'll reuse this debouncing logic, consider moving it to a utility function or into the PackageSearchService itself.

安全:XSRF 防護

Security: XSRF protection

跨站請求偽造 (XSRF 或 CSRF)是一個攻擊技術,它能讓攻擊者假冒一個已認證的使用者在你的網站上執行未知的操作。HttpClient 支援一種通用的機制來防範 XSRF 攻擊。當執行 HTTP 請求時,一個攔截器會從 cookie 中讀取 XSRF 令牌(預設名字為 XSRF-TOKEN),並且把它設定為一個 HTTP 頭 X-XSRF-TOKEN,由於只有執行在你自己的域名下的程式碼才能讀取這個 cookie,因此後端可以確認這個 HTTP 請求真的來自你的客戶端應用,而不是攻擊者。

Cross-Site Request Forgery (XSRF or CSRF) is an attack technique by which the attacker can trick an authenticated user into unknowingly executing actions on your website. HttpClient supports a common mechanism used to prevent XSRF attacks. When performing HTTP requests, an interceptor reads a token from a cookie, by default XSRF-TOKEN, and sets it as an HTTP header, X-XSRF-TOKEN. Since only code that runs on your domain could read the cookie, the backend can be certain that the HTTP request came from your client application and not an attacker.

預設情況下,攔截器會在所有的修改型請求中(比如 POST 等)把這個請求頭髮送給使用相對 URL 的請求。但不會在 GET/HEAD 請求中傳送,也不會發送給使用絕對 URL 的請求。

By default, an interceptor sends this header on all mutating requests (such as POST) to relative URLs, but not on GET/HEAD requests or on requests with an absolute URL.

要獲得這種優點,你的伺服器需要在頁面載入或首個 GET 請求中把一個名叫 XSRF-TOKEN 的令牌寫入可被 JavaScript 讀到的會話 cookie 中。 而在後續的請求中,伺服器可以驗證這個 cookie 是否與 HTTP 頭 X-XSRF-TOKEN 的值一致,以確保只有執行在你自己域名下的程式碼才能發起這個請求。這個令牌必須對每個使用者都是唯一的,並且必須能被伺服器驗證,因此不能由客戶端自己產生令牌。把這個令牌設定為你的站點認證資訊並且加了鹽(salt)的摘要,以提升安全性。

To take advantage of this, your server needs to set a token in a JavaScript readable session cookie called XSRF-TOKEN on either the page load or the first GET request. On subsequent requests the server can verify that the cookie matches the X-XSRF-TOKEN HTTP header, and therefore be sure that only code running on your domain could have sent the request. The token must be unique for each user and must be verifiable by the server; this prevents the client from making up its own tokens. Set the token to a digest of your site's authentication cookie with a salt for added security.

為了防止多個 Angular 應用共享同一個域名或子域時出現衝突,要給每個應用分配一個唯一的 cookie 名稱。

In order to prevent collisions in environments where multiple Angular apps share the same domain or subdomain, give each application a unique cookie name.

HttpClient 支援的只是 XSRF 防護方案的客戶端這一半。 你的後端服務必須配置為給頁面設定 cookie,並且要驗證請求頭,以確保全都是合法的請求。如果不這麼做,就會導致 Angular 的預設防護措施失效。

HttpClient supports only the client half of the XSRF protection scheme. Your backend service must be configured to set the cookie for your page, and to verify that the header is present on all eligible requests. Failing to do so renders Angular's default protection ineffective.

配置自訂 cookie/header 名稱

Configuring custom cookie/header names

如果你的後端服務中對 XSRF 令牌的 cookie 或 頭使用了不一樣的名字,就要使用 HttpClientXsrfModule.withConfig() 來覆蓋掉預設值。

If your backend service uses different names for the XSRF token cookie or header, use HttpClientXsrfModule.withOptions() to override the defaults.

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

測試 HTTP 請求

Testing HTTP requests

如同所有的外部依賴一樣,你必須把 HTTP 後端也 Mock 掉,以便你的測試可以模擬這種與後端的互動。 @angular/common/http/testing 函式庫能讓這種 Mock 工作變得直截了當。

As for any external dependency, you must mock the HTTP backend so your tests can simulate interaction with a remote server. The @angular/common/http/testing library makes it straightforward to set up such mocking.

Angular 的 HTTP 測試函式庫是專為其中的測試模式而設計的。在這種模式下,會首先在應用中執行程式碼併發起請求。 然後,這個測試會期待發起或未發起過某個請求,並針對這些請求進行斷言, 最終對每個所預期的請求進行重新整理(flush)來對這些請求提供響應。

Angular's HTTP testing library is designed for a pattern of testing in which the app executes code and makes requests first. The test then expects that certain requests have or have not been made, performs assertions against those requests, and finally provides responses by "flushing" each expected request.

最終,測試可能會驗證這個應用不曾發起過非預期的請求。

At the end, tests can verify that the app has made no unexpected requests.

你可以到線上程式設計環境中執行這些範例測試這些範例測試 / 下載範例

You can runthese sample teststhese sample tests / 下載範例in a live coding environment.

本章所講的這些測試位於 src/testing/http-client.spec.ts 中。 在 src/app/heroes/heroes.service.spec.ts 中還有一些測試,用於測試那些呼叫了 HttpClient 的資料服務。

The tests described in this guide are in src/testing/http-client.spec.ts. There are also tests of an application data service that call HttpClient in src/app/heroes/heroes.service.spec.ts.

搭建測試環境

Setup for testing

要開始測試那些透過 HttpClient 發起的請求,就要匯入 HttpClientTestingModule 模組,並把它加到你的 TestBed 設定裡去,程式碼如下:

To begin testing calls to HttpClient, import the HttpClientTestingModule and the mocking controller, HttpTestingController, along with the other symbols your tests require.

// 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';
app/testing/http-client.spec.ts (imports)
      
      // 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 中,並繼續設定被測服務

Then add the HttpClientTestingModule to the TestBed and continue with the setup of the service-under-test.

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 /// });
app/testing/http-client.spec.ts(setup)
      
      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 ///
});
    

現在,在測試中發起的這些請求會發給這些測試用的後端(testing backend),而不是標準的後端。

Now requests made in the course of your tests hit the testing backend instead of the normal backend.

這種設定還會呼叫 TestBed.inject(),來獲取注入的 HttpClient 服務和模擬物件的控制器 HttpTestingController,以便在測試期間參考它們。

This setup also calls TestBed.inject() to inject the HttpClient service and the mocking controller so they can be referenced during the tests.

期待並回復請求

Expecting and answering requests

現在,你就可以編寫測試,等待 GET 請求並給出模擬響應。

Now you can write a test that expects a GET Request to occur and provides a mock response.

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(); });
app/testing/http-client.spec.ts(httpClient.get)
      
      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();
});
    

最後一步,驗證沒有發起過預期之外的請求,足夠通用,因此你可以把它移到 afterEach() 中:

The last step, verifying that no requests remain outstanding, is common enough for you to move it into an afterEach() step:

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

自訂對請求的預期

Custom request expectations

如果僅根據 URL 匹配還不夠,你還可以自行實現匹配函式。 比如,你可以驗證外發的請求是否帶有某個認證頭:

If matching by URL isn't sufficient, it's possible to implement your own matching function. For example, you could look for an outgoing request that has an authorization header:

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

像前面的 expectOne() 測試一樣,如果零或兩個以上的請求滿足了這個斷言,它就會丟擲異常。

As with the previous expectOne(), the test fails if 0 or 2+ requests satisfy this predicate.

處理一個以上的請求

Handling more than one request

如果你需要在測試中對重複的請求進行響應,可以使用 match() API 來代替 expectOne(),它的引數不變,但會返回一個與這些請求相匹配的陣列。一旦返回,這些請求就會從將來要匹配的列表中移除,你要自己驗證和重新整理(flush)它。

If you need to respond to duplicate requests in your test, use the match() API instead of expectOne(). It takes the same arguments but returns an array of matching requests. Once returned, these requests are removed from future matching and you are responsible for flushing and verifying them.

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

測試對錯誤的預期

Testing for errors

你還要測試應用對於 HTTP 請求失敗時的防護。

You should test the app's defenses against HTTP requests that fail.

呼叫 request.flush() 並傳入一個錯誤資訊,如下所示:

Call request.flush() with an error message, as seen in the following example.

it('can test for 404 error', () => { const emsg = 'deliberate 404 error'; httpClient.get<Data[]>(testUrl).subscribe( data => fail('should have failed with the 404 error'), (error: HttpErrorResponse) => { expect(error.status).toEqual(404, 'status'); expect(error.error).toEqual(emsg, 'message'); } ); const req = httpTestingController.expectOne(testUrl); // Respond with mock error req.flush(emsg, { status: 404, statusText: 'Not Found' }); });
      
      it('can test for 404 error', () => {
  const emsg = 'deliberate 404 error';

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

  const req = httpTestingController.expectOne(testUrl);

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

另外,你還可以使用 ErrorEvent 來呼叫 request.error().

Alternatively, you can call request.error() with an ErrorEvent.

it('can test for network error', () => { const emsg = 'simulated network error'; httpClient.get<Data[]>(testUrl).subscribe( data => fail('should have failed with the network error'), (error: HttpErrorResponse) => { expect(error.error.message).toEqual(emsg, 'message'); } ); const req = httpTestingController.expectOne(testUrl); // Create mock ErrorEvent, raised when something goes wrong at the network level. // Connection timeout, DNS error, offline, etc const mockError = new ErrorEvent('Network error', { message: emsg, }); // Respond with mock error req.error(mockError); });
      
      it('can test for network error', () => {
  const emsg = 'simulated network error';

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

  const req = httpTestingController.expectOne(testUrl);

  // Create mock ErrorEvent, raised when something goes wrong at the network level.
  // Connection timeout, DNS error, offline, etc
  const mockError = new ErrorEvent('Network error', {
    message: emsg,
  });

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