在這節(jié)課中,你將借助 Angular 的 ?HttpClient
?來添加一些數(shù)據(jù)持久化特性。
HeroService
?通過 HTTP 請求獲取英雄數(shù)據(jù)要查看本頁所講的范例程序,參閱現(xiàn)場演練 / 下載范例。
?HttpClient
?是 Angular 通過 HTTP 與遠程服務(wù)器通訊的機制。
要讓 ?HttpClient
?在應(yīng)用中隨處可用,需要兩個步驟。首先,用導入語句把它添加到根模塊 ?AppModule
?中:
import { HttpClientModule } from '@angular/common/http';
接下來,仍然在 ?AppModule
?中,把 ?HttpClientModule
?添加到 ?imports
?數(shù)組中:
@NgModule({
imports: [
HttpClientModule,
],
})
這個教學例子會與一個使用 內(nèi)存 Web API(In-memory Web API) 模擬出的遠程數(shù)據(jù)服務(wù)器通訊。
安裝完這個模塊之后,應(yīng)用將會通過 ?HttpClient
?來發(fā)起請求和接收響應(yīng),而不用在乎實際上是這個內(nèi)存 Web API 在攔截這些請求、操作一個內(nèi)存數(shù)據(jù)庫,并且給出仿真的響應(yīng)。
通過使用內(nèi)存 Web API,你不用架設(shè)服務(wù)器就可以學習 ?HttpClient
?了。
重要:
這個內(nèi)存 Web API 模塊與 Angular 中的 HTTP 模塊無關(guān)。
如果你只是在閱讀本教程來學習 ?HttpClient
?,那么可以跳過這一步。 如果你正在隨著本教程敲代碼,那就留下來,并加上這個內(nèi)存 Web API。
用如下命令從 ?npm
?中安裝這個內(nèi)存 Web API 包(譯注:請使用 0.5+ 的版本,不要使用 0.4-)
npm install angular-in-memory-web-api --save
在 ?AppModule
?中,導入 ?HttpClientInMemoryWebApiModule
?和 ?InMemoryDataService
?類,稍后你將創(chuàng)建它們。
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
在 ?HttpClientModule
?之后,將 ?HttpClientInMemoryWebApiModule
?添加到 ?AppModule
?的 ?imports
?數(shù)組中,并以 ?InMemoryDataService
?為參數(shù)對其進行配置。
HttpClientModule,
// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
// and returns simulated server responses.
// Remove it when a real server is ready to receive requests.
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, { dataEncapsulation: false }
)
?forRoot()
? 配置方法接收一個 ?InMemoryDataService
?類來初始化內(nèi)存數(shù)據(jù)庫。
使用以下命令生成類 ?src/app/in-memory-data.service.ts
?:
ng generate service InMemoryData
將 ?in-memory-data.service.ts
? 改為以下內(nèi)容:
import { Injectable } from '@angular/core';
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Hero } from './hero';
@Injectable({
providedIn: 'root',
})
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 12, name: 'Dr. Nice' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr. IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
return {heroes};
}
// Overrides the genId method to ensure that a hero always has an id.
// If the heroes array is empty,
// the method below returns the initial number (11).
// if the heroes array is not empty, the method below returns the highest
// hero id + 1.
genId(heroes: Hero[]): number {
return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11;
}
}
?in-memory-data.service.ts
? 文件已代替了 ?mock-heroes.ts
? 文件,現(xiàn)在后者可以安全的刪除了。
等服務(wù)器就緒后,你就可以拋棄這個內(nèi)存 Web API,應(yīng)用的請求將直接傳給服務(wù)器。
在 ?HeroService
?中,導入 ?HttpClient
?和 ?HttpHeaders
?:
import { HttpClient, HttpHeaders } from '@angular/common/http';
仍然在 ?HeroService
?中,把 ?HttpClient
?注入到構(gòu)造函數(shù)中一個名叫 ?http
?的私有屬性中。
constructor(
private http: HttpClient,
private messageService: MessageService) { }
注意保留對 ?MessageService
?的注入,但是因為你將頻繁調(diào)用它,因此請把它包裹進一個私有的 ?log
?方法中。
/** Log a HeroService message with the MessageService */
private log(message: string) {
this.messageService.add(`HeroService: ${message}`);
}
把服務(wù)器上英雄數(shù)據(jù)資源的訪問地址 heroesURL
定義為 :base/:collectionName
的形式。這里的 base
是要請求的資源,而 collectionName
是 in-memory-data-service.ts
中的英雄數(shù)據(jù)對象。
private heroesUrl = 'api/heroes'; // URL to web api
當前的 ?HeroService.getHeroes()
? 使用 RxJS 的 ?of()
? 函數(shù)來把模擬英雄數(shù)據(jù)返回為 ?Observable<Hero[]>
? 格式。
getHeroes(): Observable<Hero[]> {
const heroes = of(HEROES);
return heroes;
}
把該方法轉(zhuǎn)換成使用 ?HttpClient
?的,代碼如下:
/** GET heroes from the server */
getHeroes(): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
}
刷新瀏覽器后,英雄數(shù)據(jù)就會從模擬服務(wù)器被成功讀取。
你用 ?http.get()
? 替換了 ?of()
?,沒有做其它修改,但是應(yīng)用仍然在正常工作,這是因為這兩個函數(shù)都返回了 ?Observable<Hero[]>
?。
所有的 ?HttpClient
?方法都會返回某個值的 RxJS ?Observable
?。
HTTP 是一個請求/響應(yīng)式協(xié)議。你發(fā)起請求,它返回單個的響應(yīng)。
通常,?Observable
?可以在一段時間內(nèi)返回多個值。但來自 ?HttpClient
?的 ?Observable
?總是發(fā)出一個值,然后結(jié)束,再也不會發(fā)出其它值。
具體到這次 ?HttpClient.get()
? 調(diào)用,它返回一個 ?Observable<Hero[]>
?,也就是“一個英雄數(shù)組的可觀察對象”。在實踐中,它也只會返回一個英雄數(shù)組。
?HttpClient.get()
? 默認情況下把響應(yīng)體當做無類型的 JSON 對象進行返回。如果指定了可選的模板類型 ?<Hero[]>
?,就會給返回你一個類型化的對象。
服務(wù)器的數(shù)據(jù) API 決定了 JSON 數(shù)據(jù)的具體形態(tài)。英雄之旅的數(shù)據(jù) API 會把英雄數(shù)據(jù)作為一個數(shù)組進行返回。
其它 API 可能在返回對象中深埋著你想要的數(shù)據(jù)。你可能要借助 RxJS 的 ?map()
? 操作符對 ?Observable
?的結(jié)果進行處理,以便把這些數(shù)據(jù)挖掘出來。
雖然不打算在此展開討論,不過你可以到范例源碼中的 ?getHeroNo404()
? 方法中找到一個使用 ?map()
? 操作符的例子。
凡事皆會出錯,特別是當你從遠端服務(wù)器獲取數(shù)據(jù)的時候。?HeroService.getHeroes()
? 方法應(yīng)該捕獲錯誤,并做適當?shù)奶幚怼?/p>
要捕獲錯誤,你就要使用 RxJS 的 ?catchError()
? 操作符來建立對 Observable 結(jié)果的處理管道(pipe)。
從 ?rxjs/operators
? 中導入 ?catchError
?符號,以及你稍后將會用到的其它操作符。
import { catchError, map, tap } from 'rxjs/operators';
現(xiàn)在,使用 ?pipe()
? 方法來擴展 ?Observable
?的結(jié)果,并給它一個 ?catchError()
? 操作符。
getHeroes(): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
catchError(this.handleError<Hero[]>('getHeroes', []))
);
}
?catchError()
? 操作符會攔截失敗的 ?Observable
?。它把錯誤對象傳給錯誤處理器,錯誤處理器會處理這個錯誤。
下面的 ?handleError()
? 方法會報告這個錯誤,并返回一個無害的結(jié)果(安全值),以便應(yīng)用能正常工作。
下面這個 ?handleError()
? 將會在很多 ?HeroService
?的方法之間共享,所以要把它通用化,以支持這些彼此不同的需求。
它不再直接處理這些錯誤,而是返回給 ?catchError
?返回一個錯誤處理函數(shù)。還要用操作名和出錯時要返回的安全值來對這個錯誤處理函數(shù)進行配置。
/**
* Handle Http operation that failed.
* Let the app continue.
*
* @param operation - name of the operation that failed
* @param result - optional value to return as the observable result
*/
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
// TODO: better job of transforming error for user consumption
this.log(`${operation} failed: ${error.message}`);
// Let the app keep running by returning an empty result.
return of(result as T);
};
}
在控制臺中匯報了這個錯誤之后,這個處理器會匯報一個用戶友好的消息,并給應(yīng)用返回一個安全值,讓應(yīng)用繼續(xù)工作。
因為每個服務(wù)方法都會返回不同類型的 ?Observable
?結(jié)果,因此 ?handleError()
? 也需要一個類型參數(shù),以便它返回一個此類型的安全值,正如應(yīng)用所期望的那樣。
?HeroService
?的方法將會窺探 ?Observable
?的數(shù)據(jù)流,并通過 ?log()
? 方法往頁面底部發(fā)送一條消息。
它們可以使用 RxJS 的 ?tap()
? 操作符來實現(xiàn),該操作符會查看 Observable 中的值,使用那些值做一些事情,并且把它們傳出來。這種 ?tap()
? 回調(diào)不會改變這些值本身。
下面是 ?getHeroes()
? 的最終版本,它使用 ?tap()
? 來記錄各種操作。
/** GET heroes from the server */
getHeroes(): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
tap(_ => this.log('fetched heroes')),
catchError(this.handleError<Hero[]>('getHeroes', []))
);
}
大多數(shù)的 Web API 都支持以 ?:baseURL/:id
? 的形式根據(jù) id 進行獲取。
這里的 ?baseURL
?就是在上面 英雄列表與 HTTP 部分定義過的 ?heroesURL
?(?api/heroes
?)。而 ?id
?則是你要獲取的英雄的編號,比如,?api/heroes/11
?。
把 ?HeroService.getHero()
? 方法改成這樣,以發(fā)起該請求:
/** GET hero by id. Will 404 if id not found */
getHero(id: number): Observable<Hero> {
const url = `${this.heroesUrl}/${id}`;
return this.http.get<Hero>(url).pipe(
tap(_ => this.log(`fetched hero id=${id}`)),
catchError(this.handleError<Hero>(`getHero id=${id}`))
);
}
這里和 ?getHeroes()
? 相比有三個顯著的差異:
getHero()
? 使用想獲取的英雄的 id 構(gòu)造了一個請求 URLgetHero()
? 會返回 ?Observable<Hero>
?(“一個可觀察的單個英雄對象”),而不是一個可觀察的英雄對象數(shù)組
在英雄詳情視圖中編輯英雄的名字。隨著輸入,英雄的名字也跟著在頁面頂部的標題區(qū)更新了。但是當你點擊“后退”按鈕時,這些修改都丟失了。
如果你希望保留這些修改,就要把它們寫回到服務(wù)器。
在英雄詳情模板的底部添加一個保存按鈕,它綁定了一個 ?click
?事件,事件綁定會調(diào)用組件中一個名叫 ?save()
? 的新方法。
<button type="button" (click)="save()">save</button>
在 ?HeroDetail
?組件類中,添加如下的 ?save()
? 方法,它使用英雄服務(wù)中的 ?updateHero()
? 方法來保存對英雄名字的修改,然后導航回前一個視圖。
save(): void {
if (this.hero) {
this.heroService.updateHero(this.hero)
.subscribe(() => this.goBack());
}
}
?updateHero()
? 的總體結(jié)構(gòu)和 ?getHeroes()
? 很相似,但它會使用 ?http.put()
? 來把修改后的英雄保存到服務(wù)器上。把下列代碼添加進 ?HeroService
?。
/** PUT: update the hero on the server */
updateHero(hero: Hero): Observable<any> {
return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
tap(_ => this.log(`updated hero id=${hero.id}`)),
catchError(this.handleError<any>('updateHero'))
);
}
?HttpClient.put()
? 方法接受三個參數(shù):
URL 沒變。英雄 Web API 通過英雄對象的 ?id
?就可以知道要修改哪個英雄。
英雄 Web API 期待在保存時的請求中有一個特殊的頭。這個頭是在 ?HeroService
?的 ?httpOptions
?常量中定義的。
httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
刷新瀏覽器,修改英雄名,保存這些修改。在 ?HeroDetailComponent
?的 ?save()
? 方法中導航到前一個視圖?,F(xiàn)在,改名后的英雄已經(jīng)顯示在列表中了。
要添加英雄,本應(yīng)用中只需要英雄的名字。你可以使用一個和添加按鈕成對的 ?<input>
? 元素。
把下列代碼插入到 ?HeroesComponent
?模板中標題的緊后面:
<div>
<label for="new-hero">Hero name: </label>
<input id="new-hero" #heroName />
<!-- (click) passes input value to add() and then clears the input -->
<button type="button" class="add-button" (click)="add(heroName.value); heroName.value=''">
Add hero
</button>
</div>
當點擊事件觸發(fā)時,調(diào)用組件的點擊處理器(?add()
?),然后清空這個輸入框,以便用來輸入另一個名字。把下列代碼添加到 ?HeroesComponent
?類:
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.addHero({ name } as Hero)
.subscribe(hero => {
this.heroes.push(hero);
});
}
當指定的名字非空時,這個處理器會用這個名字創(chuàng)建一個類似于 ?Hero
?的對象(只缺少 ?id
?屬性),并把它傳給服務(wù)的 ?addHero()
? 方法。
當 ?addHero()
? 保存成功時,?subscribe()
? 的回調(diào)函數(shù)會收到這個新英雄,并把它追加到 ?heroes
?列表中以供顯示。
往 ?HeroService
?類中添加 ?addHero()
? 方法。
/** POST: add a new hero to the server */
addHero(hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
catchError(this.handleError<Hero>('addHero'))
);
}
?addHero()
? 和 ?updateHero()
? 有兩點不同。
HttpClient.post()
? 而不是 ?put()
?。Observable<Hero>
? 返回給調(diào)用者。刷新瀏覽器,并添加一些英雄。
英雄列表中的每個英雄都有一個刪除按鈕。
把下列按鈕(?button
?)元素添加到 ?HeroesComponent
?的模板中,就在每個 ?<li>
? 元素中的英雄名字后方。
<button type="button" class="delete" title="delete hero"
(click)="delete(hero)">x</button>
英雄列表的 HTML 應(yīng)該是這樣的:
<ul class="heroes">
<li *ngFor="let hero of heroes">
<a routerLink="/detail/{{hero.id}}">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</a>
<button type="button" class="delete" title="delete hero"
(click)="delete(hero)">x</button>
</li>
</ul>
要把刪除按鈕定位在每個英雄條目的最右邊,就要往 ?heroes.component.css
? 中添加一些 CSS。你可以在下方的 最終代碼 中找到這些 CSS。
把 ?delete()
? 處理器添加到組件中。
delete(hero: Hero): void {
this.heroes = this.heroes.filter(h => h !== hero);
this.heroService.deleteHero(hero.id).subscribe();
}
雖然這個組件把刪除英雄的邏輯委托給了 ?HeroService
?,但仍保留了更新它自己的英雄列表的職責。組件的 ?delete()
? 方法會在 ?HeroService
?對服務(wù)器的操作成功之前,先從列表中移除要刪除的英雄。
組件與 ?heroService.deleteHero()
? 返回的 ?Observable
?還完全沒有關(guān)聯(lián)。必須訂閱它。
如果你忘了調(diào)用 ?subscribe()
?,本服務(wù)將不會把這個刪除請求發(fā)送給服務(wù)器。作為一條通用的規(guī)則,?Observable
?在有人訂閱之前什么都不會做。
你可以暫時刪除 ?subscribe()
? 來確認這一點。點擊“Dashboard”,然后點擊“Heroes”,就又看到完整的英雄列表了。
接下來,把 ?deleteHero()
? 方法添加到 ?HeroService
?中,代碼如下。
/** DELETE: delete the hero from the server */
deleteHero(id: number): Observable<Hero> {
const url = `${this.heroesUrl}/${id}`;
return this.http.delete<Hero>(url, this.httpOptions).pipe(
tap(_ => this.log(`deleted hero id=${id}`)),
catchError(this.handleError<Hero>('deleteHero'))
);
}
注意以下關(guān)鍵點:
deleteHero()
? 調(diào)用了 ?HttpClient.delete()
?id
?put()
? 和 ?post()
? 中那樣發(fā)送任何數(shù)據(jù)httpOptions
?刷新瀏覽器,并試一下這個新的刪除功能。
在最后一次練習中,你要學到把 ?Observable
?的操作符串在一起,讓你能將相似 HTTP 請求的數(shù)量最小化,并節(jié)省網(wǎng)絡(luò)帶寬。
你將往儀表盤中加入英雄搜索特性。當用戶在搜索框中輸入名字時,你會不斷發(fā)送根據(jù)名字過濾英雄的 HTTP 請求。你的目標是僅僅發(fā)出盡可能少的必要請求。
先把 ?searchHeroes()
? 方法添加到 ?HeroService
?中。
/* GET heroes whose name contains search term */
searchHeroes(term: string): Observable<Hero[]> {
if (!term.trim()) {
// if not search term, return empty hero array.
return of([]);
}
return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
tap(x => x.length ?
this.log(`found heroes matching "${term}"`) :
this.log(`no heroes matching "${term}"`)),
catchError(this.handleError<Hero[]>('searchHeroes', []))
);
}
如果沒有搜索詞,該方法立即返回一個空數(shù)組。剩下的部分和 ?getHeroes()
? 很像。唯一的不同點是 URL,它包含了一個由搜索詞組成的查詢字符串。
打開 ?DashboardComponent
?的模板并且把用于搜索英雄的元素 ?<app-hero-search>
? 添加到代碼的底部。
<h2>Top Heroes</h2>
<div class="heroes-menu">
<a *ngFor="let hero of heroes"
routerLink="/detail/{{hero.id}}">
{{hero.name}}
</a>
</div>
<app-hero-search></app-hero-search>
這個模板看起來很像 ?HeroesComponent
?模板中的 ?*ngFor
? 復寫器。
為此,下一步就是添加一個組件,它的選擇器要能匹配 ?<app-hero-search>
?。
使用 CLI 創(chuàng)建一個 ?HeroSearchComponent
?。
ng generate component hero-search
CLI 生成了 ?HeroSearchComponent
?的三個文件,并把該組件添加到了 ?AppModule
?的聲明中。
把生成的 ?HeroSearchComponent
?的模板改成一個 ?<input>
? 和一個匹配到的搜索結(jié)果的列表。代碼如下。
<div id="search-component">
<label for="search-box">Hero Search</label>
<input #searchBox id="search-box" (input)="search(searchBox.value)" />
<ul class="search-result">
<li *ngFor="let hero of heroes$ | async" >
<a routerLink="/detail/{{hero.id}}">
{{hero.name}}
</a>
</li>
</ul>
</div>
從下面的 最終代碼 中把私有 CSS 樣式添加到 ?hero-search.component.css
? 中。
當用戶在搜索框中輸入時,一個 ?input
?事件綁定會調(diào)用該組件的 ?search()
? 方法,并傳入新的搜索框的值。
?*ngFor
? 會重復渲染這些英雄對象。注意,?*ngFor
? 在一個名叫 ?heroes$
? 的列表上迭代,而不是 ?heroes
?。?$
? 是一個約定,表示 ?heroes$
? 是一個 ?Observable
?而不是數(shù)組。
<li *ngFor="let hero of heroes$ | async" >
由于 ?*ngFor
? 不能直接使用 ?Observable
?,所以要使用一個管道字符(?|
?),后面緊跟著一個 ?async
?。這表示 Angular 的 ?AsyncPipe
?管道,它會自動訂閱 ?Observable
?,這樣你就不用在組件類中這么做了。
修改所生成的 ?HeroSearchComponent
?類及其元數(shù)據(jù),代碼如下。
import { Component, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import {
debounceTime, distinctUntilChanged, switchMap
} from 'rxjs/operators';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-hero-search',
templateUrl: './hero-search.component.html',
styleUrls: [ './hero-search.component.css' ]
})
export class HeroSearchComponent implements OnInit {
heroes$!: Observable<Hero[]>;
private searchTerms = new Subject<string>();
constructor(private heroService: HeroService) {}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
ngOnInit(): void {
this.heroes$ = this.searchTerms.pipe(
// wait 300ms after each keystroke before considering the term
debounceTime(300),
// ignore new term if same as previous term
distinctUntilChanged(),
// switch to new search observable each time the term changes
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
}
}
注意,?heroes$
? 聲明為一個 ?Observable
?
heroes$!: Observable<Hero[]>;
你將會在 ?ngOnInit()
? 中設(shè)置它,在此之前,先仔細看看 ?searchTerms
?的定義。
?searchTerms
?屬性是 RxJS 的 ?Subject
?類型。
private searchTerms = new Subject<string>();
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
?Subject
?既是可觀察對象的數(shù)據(jù)源,本身也是 ?Observable
?。你可以像訂閱任何 ?Observable
?一樣訂閱 ?Subject
?。
你還可以通過調(diào)用它的 ?next(value)
? 方法往 ?Observable
?中推送一些值,就像 ?search()
? 方法中一樣。
文本框的 ?input
?事件的事件綁定會調(diào)用 ?search()
? 方法。
<input #searchBox id="search-box" (input)="search(searchBox.value)" />
每當用戶在文本框中輸入時,這個事件綁定就會使用文本框的值(搜索詞)調(diào)用 search()
函數(shù)。searchTerms
變成了一個能發(fā)出搜索詞的穩(wěn)定的流。
如果每當用戶按鍵后就直接調(diào)用 ?searchHeroes()
? 將導致創(chuàng)建海量的 HTTP 請求,浪費服務(wù)器資源并干擾數(shù)據(jù)調(diào)度計劃。
應(yīng)該怎么做呢??ngOnInit()
? 往 ?searchTerms
?這個可觀察對象的處理管道中加入了一系列 RxJS 操作符,用以縮減對 ?searchHeroes()
? 的調(diào)用次數(shù),并最終返回一個可及時給出英雄搜索結(jié)果的可觀察對象(每次都是 ?Hero[]
?)。
代碼如下:
this.heroes$ = this.searchTerms.pipe(
// wait 300ms after each keystroke before considering the term
debounceTime(300),
// ignore new term if same as previous term
distinctUntilChanged(),
// switch to new search observable each time the term changes
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
各個操作符的工作方式如下:
debounceTime(300)
? 將會等待,直到新增字符串的事件暫停了 300 毫秒。你實際發(fā)起請求的間隔永遠不會小于 300ms。distinctUntilChanged()
? 會確保只在過濾條件變化時才發(fā)送請求。switchMap()
? 會為每個從 ?debounce()
? 和 ?distinctUntilChanged()
? 中通過的搜索詞調(diào)用搜索服務(wù)。它會取消并丟棄以前的搜索可觀察對象,只保留最近的。借助 switchMap 操作符,每個有效的按鍵事件都會觸發(fā)一次 ?HttpClient.get()
? 方法調(diào)用。即使在每個請求之間都有至少 300ms 的間隔,仍然可能會同時存在多個尚未返回的 HTTP 請求。
?switchMap()
? 會記住原始的請求順序,只會返回最近一次 HTTP 方法調(diào)用的結(jié)果。以前的那些請求都會被取消和舍棄。注意:
取消前一個 ?searchHeroes()
? 可觀察對象并不會中止尚未完成的 HTTP 請求。那些不想要的結(jié)果只會在它們抵達應(yīng)用代碼之前被舍棄。
記住,組件類中并沒有訂閱 ?heroes$
? 這個可觀察對象,而是由模板中的 ?AsyncPipe
?完成的。
再次運行本應(yīng)用。在這個 儀表盤 中,在搜索框中輸入一些文字。如果你輸入的字符匹配上了任何現(xiàn)有英雄的名字,你將會看到如下效果。
本文討論過的代碼文件如下(都位于 ?src/app/
? 文件夾中)。
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { Hero } from './hero';
import { MessageService } from './message.service';
@Injectable({ providedIn: 'root' })
export class HeroService {
private heroesUrl = 'api/heroes'; // URL to web api
httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
constructor(
private http: HttpClient,
private messageService: MessageService) { }
/** GET heroes from the server */
getHeroes(): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
tap(_ => this.log('fetched heroes')),
catchError(this.handleError<Hero[]>('getHeroes', []))
);
}
/** GET hero by id. Return `undefined` when id not found */
getHeroNo404<Data>(id: number): Observable<Hero> {
const url = `${this.heroesUrl}/?id=${id}`;
return this.http.get<Hero[]>(url)
.pipe(
map(heroes => heroes[0]), // returns a {0|1} element array
tap(h => {
const outcome = h ? 'fetched' : 'did not find';
this.log(`${outcome} hero id=${id}`);
}),
catchError(this.handleError<Hero>(`getHero id=${id}`))
);
}
/** GET hero by id. Will 404 if id not found */
getHero(id: number): Observable<Hero> {
const url = `${this.heroesUrl}/${id}`;
return this.http.get<Hero>(url).pipe(
tap(_ => this.log(`fetched hero id=${id}`)),
catchError(this.handleError<Hero>(`getHero id=${id}`))
);
}
/* GET heroes whose name contains search term */
searchHeroes(term: string): Observable<Hero[]> {
if (!term.trim()) {
// if not search term, return empty hero array.
return of([]);
}
return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
tap(x => x.length ?
this.log(`found heroes matching "${term}"`) :
this.log(`no heroes matching "${term}"`)),
catchError(this.handleError<Hero[]>('searchHeroes', []))
);
}
//////// Save methods //////////
/** POST: add a new hero to the server */
addHero(hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
catchError(this.handleError<Hero>('addHero'))
);
}
/** DELETE: delete the hero from the server */
deleteHero(id: number): Observable<Hero> {
const url = `${this.heroesUrl}/${id}`;
return this.http.delete<Hero>(url, this.httpOptions).pipe(
tap(_ => this.log(`deleted hero id=${id}`)),
catchError(this.handleError<Hero>('deleteHero'))
);
}
/** PUT: update the hero on the server */
updateHero(hero: Hero): Observable<any> {
return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
tap(_ => this.log(`updated hero id=${hero.id}`)),
catchError(this.handleError<any>('updateHero'))
);
}
/**
* Handle Http operation that failed.
* Let the app continue.
*
* @param operation - name of the operation that failed
* @param result - optional value to return as the observable result
*/
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
// TODO: better job of transforming error for user consumption
this.log(`${operation} failed: ${error.message}`);
// Let the app keep running by returning an empty result.
return of(result as T);
};
}
/** Log a HeroService message with the MessageService */
private log(message: string) {
this.messageService.add(`HeroService: ${message}`);
}
}
import { Injectable } from '@angular/core';
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Hero } from './hero';
@Injectable({
providedIn: 'root',
})
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 12, name: 'Dr. Nice' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr. IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
return {heroes};
}
// Overrides the genId method to ensure that a hero always has an id.
// If the heroes array is empty,
// the method below returns the initial number (11).
// if the heroes array is not empty, the method below returns the highest
// hero id + 1.
genId(heroes: Hero[]): number {
return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11;
}
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroSearchComponent } from './hero-search/hero-search.component';
import { MessagesComponent } from './messages/messages.component';
@NgModule({
imports: [
BrowserModule,
FormsModule,
AppRoutingModule,
HttpClientModule,
// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
// and returns simulated server responses.
// Remove it when a real server is ready to receive requests.
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, { dataEncapsulation: false }
)
],
declarations: [
AppComponent,
DashboardComponent,
HeroesComponent,
HeroDetailComponent,
MessagesComponent,
HeroSearchComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
<h2>My Heroes</h2>
<div>
<label for="new-hero">Hero name: </label>
<input id="new-hero" #heroName />
<!-- (click) passes input value to add() and then clears the input -->
<button type="button" class="add-button" (click)="add(heroName.value); heroName.value=''">
Add hero
</button>
</div>
<ul class="heroes">
<li *ngFor="let hero of heroes">
<a routerLink="/detail/{{hero.id}}">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</a>
<button type="button" class="delete" title="delete hero"
(click)="delete(hero)">x</button>
</li>
</ul>
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes: Hero[] = [];
constructor(private heroService: HeroService) { }
ngOnInit(): void {
this.getHeroes();
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.addHero({ name } as Hero)
.subscribe(hero => {
this.heroes.push(hero);
});
}
delete(hero: Hero): void {
this.heroes = this.heroes.filter(h => h !== hero);
this.heroService.deleteHero(hero.id).subscribe();
}
}
/* HeroesComponent's private CSS styles */
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
input {
display: block;
width: 100%;
padding: .5rem;
margin: 1rem 0;
box-sizing: border-box;
}
.heroes li {
position: relative;
cursor: pointer;
}
.heroes li:hover {
left: .1em;
}
.heroes a {
color: #333;
text-decoration: none;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
display: block;
width: 100%;
}
.heroes a:hover {
color: #2c3a41;
background-color: #e6e6e6;
}
.heroes a:active {
background-color: #525252;
color: #fafafa;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #405061;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
min-width: 16px;
text-align: right;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
.add-button {
padding: .5rem 1.5rem;
font-size: 1rem;
margin-bottom: 2rem;
}
.add-button:hover {
color: white;
background-color: #42545C;
}
button.delete {
position: absolute;
left: 210px;
top: 5px;
background-color: white;
color: #525252;
font-size: 1.1rem;
margin: 0;
padding: 1px 10px 3px 10px;
}
button.delete:hover {
background-color: #525252;
color: white;
}
<div *ngIf="hero">
<h2>{{hero.name | uppercase}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div>
<label for="hero-name">Hero name: </label>
<input id="hero-name" [(ngModel)]="hero.name" placeholder="Hero name"/>
</div>
<button type="button" (click)="goBack()">go back</button>
<button type="button" (click)="save()">save</button>
</div>
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: [ './hero-detail.component.css' ]
})
export class HeroDetailComponent implements OnInit {
hero: Hero | undefined;
constructor(
private route: ActivatedRoute,
private heroService: HeroService,
private location: Location
) {}
ngOnInit(): void {
this.getHero();
}
getHero(): void {
const id = parseInt(this.route.snapshot.paramMap.get('id')!, 10);
this.heroService.getHero(id)
.subscribe(hero => this.hero = hero);
}
goBack(): void {
this.location.back();
}
save(): void {
if (this.hero) {
this.heroService.updateHero(this.hero)
.subscribe(() => this.goBack());
}
}
}
/* HeroDetailComponent's private CSS styles */
label {
color: #435960;
font-weight: bold;
}
input {
font-size: 1em;
padding: .5rem;
}
button {
margin-top: 20px;
margin-right: .5rem;
background-color: #eee;
padding: 1rem;
border-radius: 4px;
font-size: 1rem;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #ccc;
cursor: auto;
}
<h2>Top Heroes</h2>
<div class="heroes-menu">
<a *ngFor="let hero of heroes"
routerLink="/detail/{{hero.id}}">
{{hero.name}}
</a>
</div>
<app-hero-search></app-hero-search>
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
heroes: Hero[] = [];
constructor(private heroService: HeroService) { }
ngOnInit(): void {
this.getHeroes();
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
}
}
/* DashboardComponent's private CSS styles */
h2 {
text-align: center;
}
.heroes-menu {
padding: 0;
margin: auto;
max-width: 1000px;
/* flexbox */
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
align-content: flex-start;
align-items: flex-start;
}
a {
background-color: #3f525c;
border-radius: 2px;
padding: 1rem;
font-size: 1.2rem;
text-decoration: none;
display: inline-block;
color: #fff;
text-align: center;
width: 100%;
min-width: 70px;
margin: .5rem auto;
box-sizing: border-box;
/* flexbox */
order: 0;
flex: 0 1 auto;
align-self: auto;
}
@media (min-width: 600px) {
a {
width: 18%;
box-sizing: content-box;
}
}
a:hover {
background-color: black;
}
<div id="search-component">
<label for="search-box">Hero Search</label>
<input #searchBox id="search-box" (input)="search(searchBox.value)" />
<ul class="search-result">
<li *ngFor="let hero of heroes$ | async" >
<a routerLink="/detail/{{hero.id}}">
{{hero.name}}
</a>
</li>
</ul>
</div>
import { Component, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import {
debounceTime, distinctUntilChanged, switchMap
} from 'rxjs/operators';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-hero-search',
templateUrl: './hero-search.component.html',
styleUrls: [ './hero-search.component.css' ]
})
export class HeroSearchComponent implements OnInit {
heroes$!: Observable<Hero[]>;
private searchTerms = new Subject<string>();
constructor(private heroService: HeroService) {}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
ngOnInit(): void {
this.heroes$ = this.searchTerms.pipe(
// wait 300ms after each keystroke before considering the term
debounceTime(300),
// ignore new term if same as previous term
distinctUntilChanged(),
// switch to new search observable each time the term changes
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
}
}
/* HeroSearch private styles */
label {
display: block;
font-weight: bold;
font-size: 1.2rem;
margin-top: 1rem;
margin-bottom: .5rem;
}
input {
padding: .5rem;
width: 100%;
max-width: 600px;
box-sizing: border-box;
display: block;
}
input:focus {
outline: #336699 auto 1px;
}
li {
list-style-type: none;
}
.search-result li a {
border-bottom: 1px solid gray;
border-left: 1px solid gray;
border-right: 1px solid gray;
display: inline-block;
width: 100%;
max-width: 600px;
padding: .5rem;
box-sizing: border-box;
text-decoration: none;
color: black;
}
.search-result li a:hover {
background-color: #435A60;
color: white;
}
ul.search-result {
margin-top: 0;
padding-left: 0;
}
旅程即將結(jié)束,不過你已經(jīng)收獲頗豐。
HeroService
?,以通過 web API 來加載英雄數(shù)據(jù)HeroService
?來支持 ?post()
?、?put()
? 和 ?delete()
? 方法
更多建議: