Hero guide 從服務(wù)器端獲取數(shù)據(jù)

2020-07-01 10:55 更新

您將借助 Angular 的 HttpClient 來添加一些數(shù)據(jù)持久化特性。

HeroService 通過 HTTP 請(qǐng)求獲取英雄數(shù)據(jù)。

用戶可以添加、編輯和刪除英雄,并通過 HTTP 來保存這些更改。

用戶可以根據(jù)名字搜索英雄。

啟用 HTTP 服務(wù)

HttpClient 是 Angular 通過 HTTP 與遠(yuǎn)程服務(wù)器通訊的機(jī)制。

要讓 HttpClient 在應(yīng)用中隨處可用,需要兩個(gè)步驟。首先,用導(dǎo)入語句把它添加到根模塊 AppModule 中:

Path:"src/app/app.module.ts (HttpClientModule import)"

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

接下來,仍然在 AppModule 中,把 HttpClientModule 添加到 imports 數(shù)組中:

Path:"src/app/app.module.ts (imports array excerpt)"

@NgModule({
  imports: [
    HttpClientModule,
  ],
})

模擬數(shù)據(jù)服務(wù)器

這個(gè)教學(xué)例子會(huì)與一個(gè)使用 內(nèi)存 Web API(In-memory Web API) 模擬出的遠(yuǎn)程數(shù)據(jù)服務(wù)器通訊。

安裝完這個(gè)模塊之后,應(yīng)用將會(huì)通過 HttpClient 來發(fā)起請(qǐng)求和接收響應(yīng),而不用在乎實(shí)際上是這個(gè)內(nèi)存 Web API 在攔截這些請(qǐng)求、操作一個(gè)內(nèi)存數(shù)據(jù)庫,并且給出仿真的響應(yīng)。

通過使用內(nèi)存 Web API,你不用架設(shè)服務(wù)器就可以學(xué)習(xí) HttpClient 了。

注:
- 這個(gè)內(nèi)存 Web API 模塊與 Angular 中的 HTTP 模塊無關(guān)。

  • 如果你只是在閱讀本教程來學(xué)習(xí) HttpClient,那么可以跳過這一步。 如果你正在隨著本教程敲代碼,那就留下來,并加上這個(gè)內(nèi)存 Web API。

用如下命令從 npm 或 cnpm 中安裝這個(gè)內(nèi)存 Web API 包(譯注:請(qǐng)使用 0.5+ 的版本,不要使用 0.4-)

npm install angular-in-memory-web-api --save

AppModule 中,導(dǎo)入 HttpClientInMemoryWebApiModuleInMemoryDataService 類,稍后你將創(chuàng)建它們。

Path:"src/app/app.module.ts (In-memory Web API imports)"

import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService }  from './in-memory-data.service';

HttpClientModule 之后,將 HttpClientInMemoryWebApiModule 添加到 AppModuleimports 數(shù)組中,并以 InMemoryDataService 為參數(shù)對(duì)其進(jìn)行配置。

Path:"src/app/app.module.ts (imports array excerpt)"

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() 配置方法接收一個(gè) InMemoryDataService 類來初始化內(nèi)存數(shù)據(jù)庫。

使用以下命令生成類 "src/app/in-memory-data.service.ts":

ng generate service InMemoryData

將 in-memory-data.service.ts 改為以下內(nèi)容:

Path:"src/app/in-memory-data.service.ts"

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: 11, name: 'Dr Nice' },
      { id: 12, name: 'Narco' },
      { 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ù)器就緒后,你就可以拋棄這個(gè)內(nèi)存 Web API,應(yīng)用的請(qǐng)求將直接傳給服務(wù)器。

英雄與 HTTP

在 HeroService 中,導(dǎo)入 HttpClientHttpHeaders

Path:"src/app/hero.service.ts (import HTTP symbols)"

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

仍然在 HeroService 中,把 HttpClient 注入到構(gòu)造函數(shù)中一個(gè)名叫 http 的私有屬性中。

Path:"src/app/hero.service.ts"

constructor(
  private http: HttpClient,
  private messageService: MessageService) { }

注意保留對(duì) MessageService 的注入,但是因?yàn)槟鷮㈩l繁調(diào)用它,因此請(qǐng)把它包裹進(jìn)一個(gè)私有的 log 方法中。

Path:"src/app/hero.service.ts"

/** Log a HeroService message with the MessageService */
private log(message: string) {
  this.messageService.add(`HeroService: ${message}`);
}

把服務(wù)器上英雄數(shù)據(jù)資源的訪問地址 heroesURL 定義為 :base/:collectionName 的形式。 這里的 base 是要請(qǐng)求的資源,而 collectionName 是 "in-memory-data-service.ts" 中的英雄數(shù)據(jù)對(duì)象。

Path:"src/app/hero.service.ts"

private heroesUrl = 'api/heroes';  // URL to web api

通過 HttpClient 獲取英雄

當(dāng)前的 HeroService.getHeroes() 使用 RxJS 的 of() 函數(shù)來把模擬英雄數(shù)據(jù)返回為 Observable<Hero[]> 格式。

Path:"src/app/hero.service.ts (getHeroes with RxJs 'of()')"

getHeroes(): Observable<Hero[]> {
  return of(HEROES);
}

把該方法轉(zhuǎn)換成使用 HttpClient 的,代碼如下:

Path:"src/app/hero.service.ts"

/** GET heroes from the server */
getHeroes(): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl)
}

刷新瀏覽器后,英雄數(shù)據(jù)就會(huì)從模擬服務(wù)器被成功讀取。

你用 http.get() 替換了 of(),沒有做其它修改,但是應(yīng)用仍然在正常工作,這是因?yàn)檫@兩個(gè)函數(shù)都返回了 Observable<Hero[]>。

HttpClient 的方法返回單個(gè)值

所有的 HttpClient 方法都會(huì)返回某個(gè)值的 RxJS Observable。

HTTP 是一個(gè)請(qǐng)求/響應(yīng)式協(xié)議。你發(fā)起請(qǐng)求,它返回單個(gè)的響應(yīng)。

通常,Observable 可以在一段時(shí)間內(nèi)返回多個(gè)值。 但來自 HttpClientObservable 總是發(fā)出一個(gè)值,然后結(jié)束,再也不會(huì)發(fā)出其它值。

具體到這次 HttpClient.get() 調(diào)用,它返回一個(gè) Observable<Hero[]>,也就是“一個(gè)英雄數(shù)組的可觀察對(duì)象”。在實(shí)踐中,它也只會(huì)返回一個(gè)英雄數(shù)組。

HttpClient.get() 返回響應(yīng)數(shù)據(jù)

HttpClient.get() 默認(rèn)情況下把響應(yīng)體當(dāng)做無類型的 JSON 對(duì)象進(jìn)行返回。 如果指定了可選的模板類型 <Hero[]>,就會(huì)給返回你一個(gè)類型化的對(duì)象。

服務(wù)器的數(shù)據(jù) API 決定了 JSON 數(shù)據(jù)的具體形態(tài)。 英雄指南的數(shù)據(jù) API 會(huì)把英雄數(shù)據(jù)作為一個(gè)數(shù)組進(jìn)行返回。

注:
- 其它 API 可能在返回對(duì)象中深埋著你想要的數(shù)據(jù)。 你可能要借助 RxJS 的 map() 操作符對(duì) Observable 的結(jié)果進(jìn)行處理,以便把這些數(shù)據(jù)挖掘出來。

  • 雖然不打算在此展開討論,不過你可以到范例源碼中的 getHeroNo404() 方法中找到一個(gè)使用 map() 操作符的例子。

錯(cuò)誤處理

凡事皆會(huì)出錯(cuò),特別是當(dāng)你從遠(yuǎn)端服務(wù)器獲取數(shù)據(jù)的時(shí)候。 HeroService.getHeroes() 方法應(yīng)該捕獲錯(cuò)誤,并做適當(dāng)?shù)奶幚怼?/p>

要捕獲錯(cuò)誤,你就要使用 RxJS 的 catchError() 操作符來建立對(duì)Observable 結(jié)果的處理管道(pipe)。

從 rxjs/operators 中導(dǎo)入 catchError 符號(hào),以及你稍后將會(huì)用到的其它操作符。

Path:"src/app/hero.service.ts"

import { catchError, map, tap } from 'rxjs/operators';

現(xiàn)在,使用 pipe() 方法來擴(kuò)展 Observable 的結(jié)果,并給它一個(gè) catchError() 操作符。

Path:"src/app/hero.service.ts"

getHeroes(): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl)
    .pipe(
      catchError(this.handleError<Hero[]>('getHeroes', []))
    );
}

catchError() 操作符會(huì)攔截失敗的 Observable。 它把錯(cuò)誤對(duì)象傳給錯(cuò)誤處理器,錯(cuò)誤處理器會(huì)處理這個(gè)錯(cuò)誤。

下面的 handleError() 方法會(huì)報(bào)告這個(gè)錯(cuò)誤,并返回一個(gè)無害的結(jié)果(安全值),以便應(yīng)用能正常工作。

handleError

下面這個(gè) handleError() 將會(huì)在很多 HeroService 的方法之間共享,所以要把它通用化,以支持這些彼此不同的需求。

它不再直接處理這些錯(cuò)誤,而是返回給 catchError 返回一個(gè)錯(cuò)誤處理函數(shù)。還要用操作名和出錯(cuò)時(shí)要返回的安全值來對(duì)這個(gè)錯(cuò)誤處理函數(shù)進(jìn)行配置。

Path:"src/app/hero.service.ts"

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

在控制臺(tái)中匯報(bào)了這個(gè)錯(cuò)誤之后,這個(gè)處理器會(huì)匯報(bào)一個(gè)用戶友好的消息,并給應(yīng)用返回一個(gè)安全值,讓應(yīng)用繼續(xù)工作。

因?yàn)槊總€(gè)服務(wù)方法都會(huì)返回不同類型的 Observable 結(jié)果,因此 handleError() 也需要一個(gè)類型參數(shù),以便它返回一個(gè)此類型的安全值,正如應(yīng)用所期望的那樣。

窺探 Observable

HeroService 的方法將會(huì)窺探 Observable 的數(shù)據(jù)流,并通過 log() 方法往頁面底部發(fā)送一條消息。

它們可以使用 RxJS 的 tap() 操作符來實(shí)現(xiàn),該操作符會(huì)查看 Observable 中的值,使用那些值做一些事情,并且把它們傳出來。 這種 tap() 回調(diào)不會(huì)改變這些值本身。

下面是 getHeroes() 的最終版本,它使用 tap() 來記錄各種操作。

Path:"src/app/hero.service.ts"

/** 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', []))
    );
}

通過 id 獲取英雄

大多數(shù)的 Web API 都支持以 :baseURL/:id 的形式根據(jù) id 進(jìn)行獲取。

這里的 baseURL 就是在 英雄列表與 HTTP 部分定義過的 heroesURL(api/heroes)。而 id 則是你要獲取的英雄的編號(hào),比如,api/heroes/11。 把 HeroService.getHero() 方法改成這樣,以發(fā)起該請(qǐng)求:

Path:"src/app/hero.service.ts"

/** 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() 相比有三個(gè)顯著的差異:

  • getHero() 使用想獲取的英雄的 id 構(gòu)造了一個(gè)請(qǐng)求 URL。

  • 服務(wù)器應(yīng)該使用單個(gè)英雄作為回應(yīng),而不是一個(gè)英雄數(shù)組。

  • 所以,getHero() 會(huì)返回 Observable<Hero>(“一個(gè)可觀察的單個(gè)英雄對(duì)象”),而不是一個(gè)可觀察的英雄對(duì)象數(shù)組。

修改英雄

在英雄詳情視圖中編輯英雄的名字。 隨著輸入,英雄的名字也跟著在頁面頂部的標(biāo)題區(qū)更新了。 但是當(dāng)你點(diǎn)擊“后退”按鈕時(shí),這些修改都丟失了。

如果你希望保留這些修改,就要把它們寫回到服務(wù)器。

在英雄詳情模板的底部添加一個(gè)保存按鈕,它綁定了一個(gè) click 事件,事件綁定會(huì)調(diào)用組件中一個(gè)名叫 save() 的新方法:

Path:"src/app/hero-detail/hero-detail.component.html (save)"

<button (click)="save()">save</button>

在 HeroDetail 組件類中,添加如下的 save() 方法,它使用英雄服務(wù)中的 updateHero() 方法來保存對(duì)英雄名字的修改,然后導(dǎo)航回前一個(gè)視圖。

Path:"src/app/hero-detail/hero-detail.component.ts (save)"

save(): void {
  this.heroService.updateHero(this.hero)
    .subscribe(() => this.goBack());
}

添加 HeroService.updateHero()

updateHero() 的總體結(jié)構(gòu)和 getHeroes() 很相似,但它會(huì)使用 http.put() 來把修改后的英雄保存到服務(wù)器上。 把下列代碼添加進(jìn) HeroService。

Path:"src/app/hero.service.ts (update)"

/** 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() 方法接受三個(gè)參數(shù):

  • URL 地址

  • 要修改的數(shù)據(jù)(這里就是修改后的英雄)

  • 選項(xiàng)

URL 沒變。英雄 Web API 通過英雄對(duì)象的 id 就可以知道要修改哪個(gè)英雄。

英雄 Web API 期待在保存時(shí)的請(qǐng)求中有一個(gè)特殊的頭。 這個(gè)頭是在 HeroServicehttpOptions 常量中定義的。

Path:"src/app/hero.service.ts"

httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};

刷新瀏覽器,修改英雄名,保存這些修改。在 HeroDetailComponentsave() 方法中導(dǎo)航到前一個(gè)視圖。 現(xiàn)在,改名后的英雄已經(jīng)顯示在列表中了。

添加新英雄

要添加英雄,本應(yīng)用中只需要英雄的名字。你可以使用一個(gè)和添加按鈕成對(duì)的 <input> 元素。

把下列代碼插入到 HeroesComponent 模板中標(biāo)題的緊后面:

Path:"src/app/heroes/heroes.component.html (add)"

<div>
  <label>Hero name:
    <input #heroName />
  </label>
  <!-- (click) passes input value to add() and then clears the input -->
  <button (click)="add(heroName.value); heroName.value=''">
    add
  </button>
</div>

當(dāng)點(diǎn)擊事件觸發(fā)時(shí),調(diào)用組件的點(diǎn)擊處理器(add()),然后清空這個(gè)輸入框,以便用來輸入另一個(gè)名字。把下列代碼添加到 HeroesComponent 類:

Path:"src/app/heroes/heroes.component.ts (add)"

add(name: string): void {
  name = name.trim();
  if (!name) { return; }
  this.heroService.addHero({ name } as Hero)
    .subscribe(hero => {
      this.heroes.push(hero);
    });
}

當(dāng)指定的名字非空時(shí),這個(gè)處理器會(huì)用這個(gè)名字創(chuàng)建一個(gè)類似于 Hero 的對(duì)象(只缺少 id 屬性),并把它傳給服務(wù)的 addHero() 方法。

當(dāng) addHero() 保存成功時(shí),subscribe() 的回調(diào)函數(shù)會(huì)收到這個(gè)新英雄,并把它追加到 heroes 列表中以供顯示。

HeroService 類中添加 addHero() 方法。

Path:"src/app/hero.service.ts (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() 有兩點(diǎn)不同。

它調(diào)用 HttpClient.post() 而不是 put()。

它期待服務(wù)器為這個(gè)新的英雄生成一個(gè) id,然后把它通過 Observable<Hero> 返回給調(diào)用者。

刷新瀏覽器,并添加一些英雄。

刪除某個(gè)英雄

英雄列表中的每個(gè)英雄都有一個(gè)刪除按鈕。

把下列按鈕(button)元素添加到 HeroesComponent 的模板中,就在每個(gè) <li>元素中的英雄名字后方。

Path:"src/app/heroes/heroes.component.html"

<button class="delete" title="delete hero"
  (click)="delete(hero)">x</button>

英雄列表的 HTML 應(yīng)該是這樣的:

Path:"src/app/heroes/heroes.component.html (list of heroes)"

<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </a>
    <button class="delete" title="delete hero"
      (click)="delete(hero)">x</button>
  </li>
</ul>

要把刪除按鈕定位在每個(gè)英雄條目的最右邊,就要往 heroes.component.css 中添加一些 CSS。你可以在下方的 最終代碼 中找到這些 CSS。

delete() 處理器添加到組件中。

Path:"src/app/heroes/heroes.component.ts (delete)"

delete(hero: Hero): void {
  this.heroes = this.heroes.filter(h => h !== hero);
  this.heroService.deleteHero(hero).subscribe();
}

雖然這個(gè)組件把刪除英雄的邏輯委托給了 HeroService,但仍保留了更新它自己的英雄列表的職責(zé)。 組件的 delete() 方法會(huì)在 HeroService 對(duì)服務(wù)器的操作成功之前,先從列表中移除要?jiǎng)h除的英雄。

組件與 heroService.delete() 返回的 Observable 還完全沒有關(guān)聯(lián)。必須訂閱它。

注:
- 如果你忘了調(diào)用 subscribe(),本服務(wù)將不會(huì)把這個(gè)刪除請(qǐng)求發(fā)送給服務(wù)器。 作為一條通用的規(guī)則,Observable 在有人訂閱之前什么都不會(huì)做。

  • 你可以暫時(shí)刪除 subscribe() 來確認(rèn)這一點(diǎn)。點(diǎn)擊“Dashboard”,然后點(diǎn)擊“Heroes”,就又看到完整的英雄列表了。

接下來,把 deleteHero() 方法添加到 HeroService 中,代碼如下。

Path:"src/app/hero.service.ts (delete)"

/** DELETE: delete the hero from the server */
deleteHero(hero: Hero | number): Observable<Hero> {
  const id = typeof hero === 'number' ? hero : hero.id;
  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'))
  );
}

注:
- deleteHero() 調(diào)用了 HttpClient.delete()。

  • URL 就是英雄的資源 URL 加上要?jiǎng)h除的英雄的 id。

  • 您不用像 put()post() 中那樣發(fā)送任何數(shù)據(jù)。

  • 您仍要發(fā)送 httpOptions。

根據(jù)名字搜索

在最后一次練習(xí)中,您要學(xué)到把 Observable 的操作符串在一起,讓你能將相似 HTTP 請(qǐng)求的數(shù)量最小化,并節(jié)省網(wǎng)絡(luò)帶寬。

您將往儀表盤中加入英雄搜索特性。 當(dāng)用戶在搜索框中輸入名字時(shí),您會(huì)不斷發(fā)送根據(jù)名字過濾英雄的 HTTP 請(qǐng)求。 您的目標(biāo)是僅僅發(fā)出盡可能少的必要請(qǐng)求。

HeroService.searchHeroes()

先把 searchHeroes() 方法添加到 HeroService 中。

Path:"src/app/hero.service.ts"

/* 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', []))
  );
}

如果沒有搜索詞,該方法立即返回一個(gè)空數(shù)組。 剩下的部分和 getHeroes() 很像。 唯一的不同點(diǎn)是 URL,它包含了一個(gè)由搜索詞組成的查詢字符串。

為儀表盤添加搜索功能

打開 DashboardComponent 的模板并且把用于搜索英雄的元素 <app-hero-search> 添加到代碼的底部。

Path:"src/app/dashboard/dashboard.component.html"

<h3>Top Heroes</h3>
<div class="grid grid-pad">
  <a *ngFor="let hero of heroes" class="col-1-4"
      routerLink="/detail/{{hero.id}}">
    <div class="module hero">
      <h4>{{hero.name}}</h4>
    </div>
  </a>
</div>

這個(gè)模板看起來很像 HeroesComponent 模板中的 *ngFor 復(fù)寫器。

為此,下一步就是添加一個(gè)組件,它的選擇器要能匹配 <app-hero-search>。

創(chuàng)建 HeroSearchComponent

使用 CLI 創(chuàng)建一個(gè) HeroSearchComponent。

ng generate component hero-search

CLI 生成了 HeroSearchComponent 的三個(gè)文件,并把該組件添加到了 AppModule 的聲明中。

把生成的 HeroSearchComponent 的模板改成一個(gè) <input> 和一個(gè)匹配到的搜索結(jié)果的列表。代碼如下:

Path:"src/app/hero-search/hero-search.component.html"

<div id="search-component">
  <h4><label for="search-box">Hero Search</label></h4>


  <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" 中。

當(dāng)用戶在搜索框中輸入時(shí),一個(gè) keyup 事件綁定會(huì)調(diào)用該組件的 search() 方法,并傳入新的搜索框的值。

AsyncPipe

*ngFor 會(huì)重復(fù)渲染這些英雄對(duì)象。注意,*ngFor 在一個(gè)名叫 heroes$ 的列表上迭代,而不是 heroes$ 是一個(gè)約定,表示 heroes$ 是一個(gè) Observable 而不是數(shù)組。

Path:"src/app/hero-search/hero-search.component.html"

<li *ngFor="let hero of heroes$ | async" >

由于 *ngFor 不能直接使用 Observable,所以要使用一個(gè)管道字符(|),后面緊跟著一個(gè) async。這表示 Angular 的 AsyncPipe 管道,它會(huì)自動(dòng)訂閱 Observable,這樣你就不用在組件類中這么做了。

修正 HeroSearchComponent 類

修改所生成的 HeroSearchComponent 類及其元數(shù)據(jù),代碼如下:

Path:"src/app/hero-search/hero-search.component.ts"

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$ 聲明為一個(gè) Observable。

Path:"src/app/hero-search/hero-search.component.ts"

heroes$: Observable<Hero[]>;

你將會(huì)在 ngOnInit() 中設(shè)置它,在此之前,先仔細(xì)看看 searchTerms 的定義。

RxJS Subject 類型的 searchTerms

searchTerms 屬性是 RxJS 的 Subject 類型。

Path:"src/app/hero-search/hero-search.component.ts"

private searchTerms = new Subject<string>();


// Push a search term into the observable stream.
search(term: string): void {
  this.searchTerms.next(term);
}

Subject 既是可觀察對(duì)象的數(shù)據(jù)源,本身也是 Observable。 你可以像訂閱任何 Observable 一樣訂閱 Subject。

你還可以通過調(diào)用它的 next(value) 方法往 Observable 中推送一些值,就像 search() 方法中一樣。

文本框的 input 事件的事件綁定會(huì)調(diào)用 search() 方法。

Path:"src/app/hero-search/hero-search.component.html"

<input #searchBox id="search-box" (input)="search(searchBox.value)" />

每當(dāng)用戶在文本框中輸入時(shí),這個(gè)事件綁定就會(huì)使用文本框的值(搜索詞)調(diào)用 search() 函數(shù)。 searchTerms 變成了一個(gè)能發(fā)出搜索詞的穩(wěn)定的流。

串聯(lián) RxJS 操作符

如果每當(dāng)用戶擊鍵后就直接調(diào)用 searchHeroes() 將導(dǎo)致創(chuàng)建海量的 HTTP 請(qǐng)求,浪費(fèi)服務(wù)器資源并干擾數(shù)據(jù)調(diào)度計(jì)劃。

應(yīng)該怎么做呢?ngOnInit()searchTerms 這個(gè)可觀察對(duì)象的處理管道中加入了一系列 RxJS 操作符,用以縮減對(duì) searchHeroes() 的調(diào)用次數(shù),并最終返回一個(gè)可及時(shí)給出英雄搜索結(jié)果的可觀察對(duì)象(每次都是 Hero[] )。

代碼如下:

Path:"src/app/hero-search/hero-search.component.ts"

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

各個(gè)操作符的工作方式如下:

  • 在傳出最終字符串之前,debounceTime(300) 將會(huì)等待,直到新增字符串的事件暫停了 300 毫秒。 你實(shí)際發(fā)起請(qǐng)求的間隔永遠(yuǎn)不會(huì)小于 300ms。

  • distinctUntilChanged() 會(huì)確保只在過濾條件變化時(shí)才發(fā)送請(qǐng)求。

  • switchMap() 會(huì)為每個(gè)從 debounce()distinctUntilChanged() 中通過的搜索詞調(diào)用搜索服務(wù)。 它會(huì)取消并丟棄以前的搜索可觀察對(duì)象,只保留最近的。

注:
- 借助 switchMap 操作符, 每個(gè)有效的擊鍵事件都會(huì)觸發(fā)一次 HttpClient.get() 方法調(diào)用。 即使在每個(gè)請(qǐng)求之間都有至少 300ms 的間隔,仍然可能會(huì)同時(shí)存在多個(gè)尚未返回的 HTTP 請(qǐng)求。

  • switchMap() 會(huì)記住原始的請(qǐng)求順序,只會(huì)返回最近一次 HTTP 方法調(diào)用的結(jié)果。 以前的那些請(qǐng)求都會(huì)被取消和舍棄。

  • 注意,取消前一個(gè) searchHeroes() 可觀察對(duì)象并不會(huì)中止尚未完成的 HTTP 請(qǐng)求。 那些不想要的結(jié)果只會(huì)在它們抵達(dá)應(yīng)用代碼之前被舍棄。

記住,組件類中并沒有訂閱 heroes$這個(gè)可觀察對(duì)象,而是由模板中的 AsyncPipe 完成的。

再次運(yùn)行本應(yīng)用,在這個(gè)儀表盤中輸入現(xiàn)有的英雄名字,您可以看到:

查看最終代碼

HeroService

Path:"src/app/hero.service.ts"

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(hero: Hero | number): Observable<Hero> {
    const id = typeof hero === 'number' ? hero : hero.id;
    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}`);
  }
}

InMemoryDataService

Path:"src/app/in-memory-data.service.ts"

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: 11, name: 'Dr Nice' },
      { id: 12, name: 'Narco' },
      { 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;
  }
}

AppModule

Path:"src/app/app.module.ts"

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

HeroesComponent

  • Path:"src/app/heroes/heroes.component.html"

<h2>My Heroes</h2>


<div>
  <label>Hero name:
    <input #heroName />
  </label>
  <!-- (click) passes input value to add() and then clears the input -->
  <button (click)="add(heroName.value); heroName.value=''">
    add
  </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 class="delete" title="delete hero"
      (click)="delete(hero)">x</button>
  </li>
</ul>

  • Path:"src/app/heroes/heroes.component.ts"

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() {
    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).subscribe();
  }


}

  • Path:"src/app/heroes/heroes.component.css"

/* HeroesComponent's private CSS styles */
.heroes {
  margin: 0 0 2em 0;
  list-style-type: none;
  padding: 0;
  width: 15em;
}
.heroes li {
  position: relative;
  cursor: pointer;
  background-color: #EEE;
  margin: .5em;
  padding: .3em 0;
  height: 1.6em;
  border-radius: 4px;
}


.heroes li:hover {
  color: #607D8B;
  background-color: #DDD;
  left: .1em;
}


.heroes a {
  color: #333;
  text-decoration: none;
  position: relative;
  display: block;
  width: 250px;
}


.heroes a:hover {
  color: #607D8B;
}


.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;
}


button {
  background-color: #eee;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  cursor: hand;
  font-family: Arial;
}


button:hover {
  background-color: #cfd8dc;
}


button.delete {
  position: relative;
  left: 194px;
  top: -32px;
  background-color: gray !important;
  color: white;
}

HeroDetailComponent

  • Path:"src/app/hero-detail/hero-detail.component.html"

<div *ngIf="hero">
  <h2>{{hero.name | uppercase}} Details</h2>
  <div><span>id: </span>{{hero.id}}</div>
  <div>
    <label>name:
      <input [(ngModel)]="hero.name" placeholder="name"/>
    </label>
  </div>
  <button (click)="goBack()">go back</button>
  <button (click)="save()">save</button>
</div>

  • Path:"src/app/hero-detail/hero-detail.component.ts"

import { Component, OnInit, Input } 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 {
  @Input() hero: Hero;


  constructor(
    private route: ActivatedRoute,
    private heroService: HeroService,
    private location: Location
  ) {}


  ngOnInit(): void {
    this.getHero();
  }


  getHero(): void {
    const id = +this.route.snapshot.paramMap.get('id');
    this.heroService.getHero(id)
      .subscribe(hero => this.hero = hero);
  }


  goBack(): void {
    this.location.back();
  }


  save(): void {
    this.heroService.updateHero(this.hero)
      .subscribe(() => this.goBack());
  }
}

DashboardComponent

Path:"src/app/dashboard/dashboard.component.html"

<h3>Top Heroes</h3>
<div class="grid grid-pad">
  <a *ngFor="let hero of heroes" class="col-1-4"
      routerLink="/detail/{{hero.id}}">
    <div class="module hero">
      <h4>{{hero.name}}</h4>
    </div>
  </a>
</div>


<app-hero-search></app-hero-search>

HeroSearchComponent

  • Path:"src/app/hero-search/hero-search.component.html"

<div id="search-component">
  <h4><label for="search-box">Hero Search</label></h4>


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

  • Path:"src/app/hero-search/hero-search.component.ts"

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)),
    );
  }
}

  • Path:"src/app/hero-search/hero-search.component.css"

/* HeroSearch private styles */
.search-result li {
  border-bottom: 1px solid gray;
  border-left: 1px solid gray;
  border-right: 1px solid gray;
  width: 195px;
  height: 16px;
  padding: 5px;
  background-color: white;
  cursor: pointer;
  list-style-type: none;
}


.search-result li:hover {
  background-color: #607D8B;
}


.search-result li a {
  color: #888;
  display: block;
  text-decoration: none;
}


.search-result li a:hover {
  color: white;
}
.search-result li a:active {
  color: white;
}
#search-box {
  width: 200px;
  height: 20px;
}




ul.search-result {
  margin-top: 0;
  padding-left: 0;
}

總結(jié)

您添加了在應(yīng)用程序中使用 HTTP 的必備依賴。

您重構(gòu)了 HeroService,以通過 web API 來加載英雄數(shù)據(jù)。

您擴(kuò)展了 HeroService 來支持 post()、put() 和 delete() 方法。

您修改了組件,以允許用戶添加、編輯和刪除英雄。

您配置了一個(gè)內(nèi)存 Web API。

您學(xué)會(huì)了如何使用“可觀察對(duì)象”。

《英雄指南》教程結(jié)束了。 如果你準(zhǔn)備開始學(xué)習(xí) Angular 開發(fā)的原理,請(qǐng)開始 架構(gòu) 一章。

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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)