Hero guide 從服務器端獲取數據

2020-07-01 10:55 更新

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

HeroService 通過 HTTP 請求獲取英雄數據。

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

用戶可以根據名字搜索英雄。

啟用 HTTP 服務

HttpClient 是 Angular 通過 HTTP 與遠程服務器通訊的機制。

要讓 HttpClient 在應用中隨處可用,需要兩個步驟。首先,用導入語句把它添加到根模塊 AppModule 中:

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

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

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

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

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

模擬數據服務器

這個教學例子會與一個使用 內存 Web API(In-memory Web API) 模擬出的遠程數據服務器通訊。

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

通過使用內存 Web API,你不用架設服務器就可以學習 HttpClient 了。

注:
- 這個內存 Web API 模塊與 Angular 中的 HTTP 模塊無關。

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

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

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

AppModule 中,導入 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 數組中,并以 InMemoryDataService 為參數對其進行配置。

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

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

ng generate service InMemoryData

將 in-memory-data.service.ts 改為以下內容:

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" 文件,現在后者可以安全的刪除了。

等服務器就緒后,你就可以拋棄這個內存 Web API,應用的請求將直接傳給服務器。

英雄與 HTTP

在 HeroService 中,導入 HttpClientHttpHeaders

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

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

仍然在 HeroService 中,把 HttpClient 注入到構造函數中一個名叫 http 的私有屬性中。

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

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

注意保留對 MessageService 的注入,但是因為您將頻繁調用它,因此請把它包裹進一個私有的 log 方法中。

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

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

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

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

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

通過 HttpClient 獲取英雄

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

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

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

把該方法轉換成使用 HttpClient 的,代碼如下:

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

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

刷新瀏覽器后,英雄數據就會從模擬服務器被成功讀取。

你用 http.get() 替換了 of(),沒有做其它修改,但是應用仍然在正常工作,這是因為這兩個函數都返回了 Observable<Hero[]>。

HttpClient 的方法返回單個值

所有的 HttpClient 方法都會返回某個值的 RxJS Observable。

HTTP 是一個請求/響應式協議。你發(fā)起請求,它返回單個的響應。

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

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

HttpClient.get() 返回響應數據

HttpClient.get() 默認情況下把響應體當做無類型的 JSON 對象進行返回。 如果指定了可選的模板類型 <Hero[]>,就會給返回你一個類型化的對象。

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

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

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

錯誤處理

凡事皆會出錯,特別是當你從遠端服務器獲取數據的時候。 HeroService.getHeroes() 方法應該捕獲錯誤,并做適當的處理。

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

從 rxjs/operators 中導入 catchError 符號,以及你稍后將會用到的其它操作符。

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

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

現在,使用 pipe() 方法來擴展 Observable 的結果,并給它一個 catchError() 操作符。

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

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

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

下面的 handleError() 方法會報告這個錯誤,并返回一個無害的結果(安全值),以便應用能正常工作。

handleError

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

它不再直接處理這些錯誤,而是返回給 catchError 返回一個錯誤處理函數。還要用操作名和出錯時要返回的安全值來對這個錯誤處理函數進行配置。

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

在控制臺中匯報了這個錯誤之后,這個處理器會匯報一個用戶友好的消息,并給應用返回一個安全值,讓應用繼續(xù)工作。

因為每個服務方法都會返回不同類型的 Observable 結果,因此 handleError() 也需要一個類型參數,以便它返回一個此類型的安全值,正如應用所期望的那樣。

窺探 Observable

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

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

下面是 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 獲取英雄

大多數的 Web API 都支持以 :baseURL/:id 的形式根據 id 進行獲取。

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

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

  • getHero() 使用想獲取的英雄的 id 構造了一個請求 URL。

  • 服務器應該使用單個英雄作為回應,而不是一個英雄數組。

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

修改英雄

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

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

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

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

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

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

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

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

添加 HeroService.updateHero()

updateHero() 的總體結構和 getHeroes() 很相似,但它會使用 http.put() 來把修改后的英雄保存到服務器上。 把下列代碼添加進 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() 方法接受三個參數:

  • URL 地址

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

  • 選項

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

英雄 Web API 期待在保存時的請求中有一個特殊的頭。 這個頭是在 HeroServicehttpOptions 常量中定義的。

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

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

刷新瀏覽器,修改英雄名,保存這些修改。在 HeroDetailComponentsave() 方法中導航到前一個視圖。 現在,改名后的英雄已經顯示在列表中了。

添加新英雄

要添加英雄,本應用中只需要英雄的名字。你可以使用一個和添加按鈕成對的 <input> 元素。

把下列代碼插入到 HeroesComponent 模板中標題的緊后面:

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>

當點擊事件觸發(fā)時,調用組件的點擊處理器(add()),然后清空這個輸入框,以便用來輸入另一個名字。把下列代碼添加到 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);
    });
}

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

addHero() 保存成功時,subscribe() 的回調函數會收到這個新英雄,并把它追加到 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() 有兩點不同。

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

它期待服務器為這個新的英雄生成一個 id,然后把它通過 Observable<Hero> 返回給調用者。

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

刪除某個英雄

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

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

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

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

英雄列表的 HTML 應該是這樣的:

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>

要把刪除按鈕定位在每個英雄條目的最右邊,就要往 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();
}

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

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

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

  • 你可以暫時刪除 subscribe() 來確認這一點。點擊“Dashboard”,然后點擊“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() 調用了 HttpClient.delete()

  • URL 就是英雄的資源 URL 加上要刪除的英雄的 id。

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

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

根據名字搜索

在最后一次練習中,您要學到把 Observable 的操作符串在一起,讓你能將相似 HTTP 請求的數量最小化,并節(jié)省網絡帶寬。

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

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

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

為儀表盤添加搜索功能

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

這個模板看起來很像 HeroesComponent 模板中的 *ngFor 復寫器。

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

創(chuàng)建 HeroSearchComponent

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

ng generate component hero-search

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

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

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

當用戶在搜索框中輸入時,一個 keyup 事件綁定會調用該組件的 search() 方法,并傳入新的搜索框的值。

AsyncPipe

*ngFor 會重復渲染這些英雄對象。注意,*ngFor 在一個名叫 heroes$ 的列表上迭代,而不是 heroes。$ 是一個約定,表示 heroes$ 是一個 Observable 而不是數組。

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

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

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

修正 HeroSearchComponent 類

修改所生成的 HeroSearchComponent 類及其元數據,代碼如下:

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$ 聲明為一個 Observable

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

heroes$: Observable<Hero[]>;

你將會在 ngOnInit() 中設置它,在此之前,先仔細看看 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 既是可觀察對象的數據源,本身也是 Observable。 你可以像訂閱任何 Observable 一樣訂閱 Subject。

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

文本框的 input 事件的事件綁定會調用 search() 方法。

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

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

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

串聯 RxJS 操作符

如果每當用戶擊鍵后就直接調用 searchHeroes() 將導致創(chuàng)建海量的 HTTP 請求,浪費服務器資源并干擾數據調度計劃。

應該怎么做呢?ngOnInit()searchTerms 這個可觀察對象的處理管道中加入了一系列 RxJS 操作符,用以縮減對 searchHeroes() 的調用次數,并最終返回一個可及時給出英雄搜索結果的可觀察對象(每次都是 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)),
);

各個操作符的工作方式如下:

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

  • distinctUntilChanged() 會確保只在過濾條件變化時才發(fā)送請求。

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

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

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

  • 注意,取消前一個 searchHeroes() 可觀察對象并不會中止尚未完成的 HTTP 請求。 那些不想要的結果只會在它們抵達應用代碼之前被舍棄。

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

再次運行本應用,在這個儀表盤中輸入現有的英雄名字,您可以看到:

查看最終代碼

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

總結

您添加了在應用程序中使用 HTTP 的必備依賴。

您重構了 HeroService,以通過 web API 來加載英雄數據。

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

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

您配置了一個內存 Web API。

您學會了如何使用“可觀察對象”。

《英雄指南》教程結束了。 如果你準備開始學習 Angular 開發(fā)的原理,請開始 架構 一章。

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號