大多數(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ù)提供了以下主要功能。
要想使用 ?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)入,看看它的配置方式。
使用 ?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 屬性。
可以用 ?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,
});
}
可以構(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)合類型,而不是普通的字符串。這會(huì)引起混亂。比如:options: { … observe?: 'body' | 'events' | 'response', … responseType?: 'arraybuffer'|'blob'|'json'|'text', … }
在第二種情況下,TypeScript 會(huì)把 ?// this works client.get('/foo', {responseType: 'text'}) // but this does NOT work const options = { responseType: 'text', }; client.get('/foo', options)
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);
在前面的例子中,對(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
?屬性。
當(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ò)誤處理程序。
不是所有的 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)求在服務(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)求。
當(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ò)誤。
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)
);
}
有時(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
);
}
除了從服務(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ù)更新方法的一些例子。
應(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
?列表中。
該應(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.
應(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)求。
很多服務(wù)器都需要額外的頭來(lái)執(zhí)行保存操作。比如,服務(wù)器可能需要一個(gè)授權(quán)令牌,或者需要 ?Content-Type
? 頭來(lái)顯式聲明請(qǐng)求體的 MIME 類型。
?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'
})
};
你不能直接修改前面的選項(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');
使用 ?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'});
借助攔截機(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ù)。
要實(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ì)象表示攔截器鏈表中的下一個(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è) ?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ò)程的直觀表示:
該過(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)求的某些特定屬性。
?readonly
?這種賦值保護(hù),無(wú)法防范深修改(修改子對(duì)象的屬性),也不能防范你修改請(qǐng)求體對(duì)象中的屬性。
req.body.name = req.body.name.trim(); // bad idea!
如果必須修改請(qǐng)求體,請(qǐng)執(zhí)行以下步驟。
clone()
? 方法克隆這個(gè)請(qǐng)求對(duì)象。// 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)求體而不是替換它。為此,請(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
以下是攔截器的一些常見(jiàn)用法。
應(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 } });
這種可以修改頭的攔截器可以用于很多不同的操作,比如:
If-Modified-Since
?因?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)用者。
攔截器可用來(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 },
攔截器還可以自行處理這些請(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)求才是可以緩存的。of()
? 函數(shù)返回一個(gè)已緩存的響應(yīng)體的可觀察對(duì)象,然后繞過(guò) ?next
?處理器(以及所有其它下游攔截器)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)的。
?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)求。
results$
?。result$
? 的管道中,使用重組后的可觀察對(duì)象進(jìn)行處理,并發(fā)出兩次。先立即發(fā)出一次緩存的響應(yīng)體,然后發(fā)出來(lái)自服務(wù)器的響應(yīng)。訂閱者將會(huì)看到一個(gè)包含這兩個(gè)響應(yīng)的序列。應(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)求。
如果你需要發(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()
? 操作符接受一個(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
?中。
跨站請(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ù)措施失效。
如果你的后端服務(wù)中對(duì) XSRF 令牌的 cookie 或 頭使用了不一樣的名字,就要使用 ?HttpClientXsrfModule.withConfig()
? 來(lái)覆蓋掉默認(rèn)值。
imports: [
HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: 'My-Xsrf-Cookie',
headerName: 'My-Xsrf-Header',
}),
],
如同所有的外部依賴一樣,你必須把 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ù)。
要開(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è)試期間引用它們。
現(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();
});
如果僅根據(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ì)拋出異常。
如果你需要在測(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è)試應(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);
});
許多攔截器都需要進(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ù)。
?HttpContextToken
?用于在上下文中存儲(chǔ)和檢索值。你可以用 ?new
?運(yùn)算符創(chuàng)建上下文令牌,如以下例所示:
export const RETRY_COUNT = new HttpContextToken(() => 3);
?HttpContextToken
?創(chuàng)建期間傳遞的 lambda 函數(shù) ?() => 3
? 有兩個(gè)用途:
HttpContextToken<number>
?。這個(gè)請(qǐng)求上下文是類型安全的 —— 從請(qǐng)求上下文中讀取令牌將返回適當(dāng)類型的值。
發(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),
);
}
}
與 ?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),
);
}
}
更多建議: