根據(jù)名字搜索
在最后一次練習中盆均,你要學到把 Observable
的操作符串在一起魏割,讓你能將相似 HTTP 請求的數(shù)量最小化橙依,并節(jié)省網(wǎng)絡帶寬。
你將往儀表盤中加入英雄搜索特性。 當用戶在搜索框中輸入名字時白嘁,你會不斷發(fā)送根據(jù)名字過濾英雄的 HTTP 請求。 你的目標是僅僅發(fā)出盡可能少的必要請求膘流。
HeroService.searchHeroes
先把 searchHeroes
方法添加到 HeroService
中权薯。
<code-example path="toh-pt6/src/app/hero.service.ts" region="searchHeroes" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><header class="ng-star-inserted" style="background-color: rgb(30, 136, 229); border-radius: 5px 5px 0px 0px; color: rgb(250, 250, 250); font-size: 16px; padding: 8px 16px;">src/app/hero.service.ts</header>
<aio-code class="headed-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy /* 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[]>(
api/heroes/?name=${term}).pipe( tap(_ => this.log(
found heroes matching "${term}")), catchError(this.handleError<Hero[]>('searchHeroes', [])) ); }
</pre></aio-code></code-example>
如果沒有搜索詞,該方法立即返回一個空數(shù)組睡扬。 剩下的部分和 getHeroes()
很像。 唯一的不同點是 URL黍析,它包含了一個由搜索詞組成的查詢字符串卖怜。
為儀表盤添加搜索功能
打開 DashboardComponent
的模板并且把用于搜索英雄的元素 <app-hero-search>
添加到 DashboardComponent
模板的底部。
<code-example path="toh-pt6/src/app/dashboard/dashboard.component.html" linenums="false" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><header class="ng-star-inserted" style="background-color: rgb(30, 136, 229); border-radius: 5px 5px 0px 0px; color: rgb(250, 250, 250); font-size: 16px; padding: 8px 16px;">src/app/dashboard/dashboard.component.html</header>
<aio-code class="headed-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy <h3>Top Heroes</h3> <div class="grid grid-pad"> <[a](https://angular.cn/api/router/RouterLinkWithHref) *[ngFor](https://angular.cn/api/common/NgForOf)="let hero of heroes" class="col-1-4" [routerLink](https://angular.cn/api/router/RouterLink)="/detail/{{hero.id}}"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </[a](https://angular.cn/api/router/RouterLinkWithHref)> </div> <app-hero-search></app-hero-search>
</pre></aio-code></code-example>
這個模板看起來很像 HeroesComponent
模板中的 *[ngFor](https://angular.cn/api/common/NgForOf)
復寫器阐枣。
很不幸马靠,添加這個元素讓本應用掛了。 Angular 找不到哪個組件的選擇器能匹配上 <app-hero-search>
蔼两。
HeroSearchComponent
還不存在甩鳄,這就解決。
創(chuàng)建 HeroSearchComponent
使用 CLI 創(chuàng)建一個 HeroSearchComponent
额划。
<code-example language="sh" class="code-shell" ng-version="5.2.0" style="clear: both; display: block; background-color: rgb(51, 51, 51); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><aio-code class="simple-code"><pre class="prettyprint lang-sh" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy ng generate component hero-search
</pre></aio-code></code-example>
CLI 生成了 HeroSearchComponent
的三個文件妙啃,并把該組件添加到了 AppModule
的聲明中。
把生成的 HeroSearchComponent
的模板改成一個輸入框和一個匹配到的搜索結(jié)果的列表。代碼如下:
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.html" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><header class="ng-star-inserted" style="background-color: rgb(30, 136, 229); border-radius: 5px 5px 0px 0px; color: rgb(250, 250, 250); font-size: 16px; padding: 8px 16px;">src/app/hero-search/hero-search.component.html</header>
<aio-code class="headed-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy <div id="search-component"> <h4>Hero Search</h4> <input #searchBox id="search-box" (keyup)="search(searchBox.value)" /> <ul class="search-result"> <li *[ngFor](https://angular.cn/api/common/NgForOf)="let hero of heroes$ | [async](https://angular.cn/api/core/testing/async)" > <[a](https://angular.cn/api/router/RouterLinkWithHref) [routerLink](https://angular.cn/api/router/RouterLink)="/detail/{{hero.id}}"> {{hero.name}} </[a](https://angular.cn/api/router/RouterLinkWithHref)> </li> </ul> </div>
</pre></aio-code></code-example>
從下面的 最終代碼 中把私有 CSS 樣式添加到 hero-search.component.css
中揖赴。
當用戶在搜索框中輸入時馆匿,一個 keyup 事件綁定會調(diào)用該組件的 search()
方法,并傳入新的搜索框的值燥滑。
AsyncPipe
如你所愿渐北,*[ngFor](https://angular.cn/api/common/NgForOf)
重復渲染出了這些英雄。
仔細看铭拧,你會發(fā)現(xiàn) *[ngFor](https://angular.cn/api/common/NgForOf)
是在一個名叫 heroes$
的列表上迭代赃蛛,而不是 heroes
。
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.html" region="async" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><aio-code class="simple-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy <li *[ngFor](https://angular.cn/api/common/NgForOf)="let hero of heroes$ | [async](https://angular.cn/api/core/testing/async)" >
</pre></aio-code></code-example>
$
是一個命名慣例搀菩,用來表明 heroes$
是一個 Observable
呕臂,而不是數(shù)組。
*[ngFor](https://angular.cn/api/common/NgForOf)
不能直接使用 Observable
秕磷。 不過诵闭,它后面還有一個管道字符(|
),后面緊跟著一個 [async](https://angular.cn/api/core/testing/async)
澎嚣,它表示 Angular 的 [AsyncPipe](https://angular.cn/api/common/AsyncPipe)
疏尿。
[AsyncPipe](https://angular.cn/api/common/AsyncPipe)
會自動訂閱到 Observable
,這樣你就不用再在組件類中訂閱了易桃。
修正 HeroSearchComponent
類
修改所生成的 HeroSearchComponent
類及其元數(shù)據(jù)褥琐,代碼如下:
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.ts" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><header class="ng-star-inserted" style="background-color: rgb(30, 136, 229); border-radius: 5px 5px 0px 0px; color: rgb(250, 250, 250); font-size: 16px; padding: 8px 16px;">src/app/hero-search/hero-search.component.ts</header>
<aio-code class="headed-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy import { [Component](https://angular.cn/api/core/Component), [OnInit](https://angular.cn/api/core/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](https://angular.cn/api/core/Component)({ selector: 'app-hero-search', templateUrl: './hero-search.component.html', styleUrls: [ './hero-search.component.css' ] }) export class HeroSearchComponent implements [OnInit](https://angular.cn/api/core/OnInit) { heroes$: Observable<Hero[]>; private searchTerms = new Subject<string>(); constructor(private heroService: HeroService) {} // Push [a](https://angular.cn/api/router/RouterLinkWithHref) 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)), ); } }
</pre></aio-code></code-example>
注意,heroes$
聲明為一個 Observable
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.ts" region="heroes-stream" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><aio-code class="simple-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy heroes$: Observable<Hero[]>;
</pre></aio-code></code-example>
你將會在 ngOnInit()
中設置它晤郑,在此之前敌呈,先仔細看看 searchTerms
的定義。
RxJS Subject
類型的 searchTerms
searchTerms
屬性聲明成了 RxJS 的 Subject
類型造寝。
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.ts" region="searchTerms" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><aio-code class="simple-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy private searchTerms = new Subject<string>(); // Push [a](https://angular.cn/api/router/RouterLinkWithHref) search term into the observable stream. search(term: string): void { this.searchTerms.next(term); }
</pre></aio-code></code-example>
Subject
既是可觀察對象的數(shù)據(jù)源磕洪,本身也是 Observable
。 你可以像訂閱任何 Observable
一樣訂閱 Subject
诫龙。
你還可以通過調(diào)用它的 next(value)
方法往 Observable
中推送一些值析显,就像 search()
方法中一樣。
search()
是通過對文本框的 keystroke
事件的事件綁定來調(diào)用的签赃。
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.html" region="input" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><aio-code class="simple-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy <input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
</pre></aio-code></code-example>
每當用戶在文本框中輸入時谷异,這個事件綁定就會使用文本框的值(搜索詞)調(diào)用 search()
函數(shù)。searchTerms
變成了一個能發(fā)出搜索詞的穩(wěn)定的流锦聊。
串聯(lián) RxJS 操作符
如果每當用戶擊鍵后就直接調(diào)用 searchHeroes()
將導致創(chuàng)建海量的 HTTP 請求歹嘹,浪費服務器資源并消耗大量網(wǎng)絡流量。
應該怎么做呢孔庭?ngOnInit()
往 searchTerms
這個可觀察對象的處理管道中加入了一系列 RxJS 操作符尺上,用以縮減對 searchHeroes()
的調(diào)用次數(shù),并最終返回一個可及時給出英雄搜索結(jié)果的可觀察對象(每次都是 Hero[]
)。
代碼如下:
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.ts" region="search" ng-version="5.2.0" style="clear: both; display: block; background-color: rgba(242, 242, 242, 0.2); border: 0.5px solid rgb(219, 219, 219); border-radius: 5px; color: rgb(51, 51, 51); margin: 16px auto; font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><aio-code class="simple-code"><pre class="prettyprint lang-" style="display: flex; min-height: 32px; margin: 16px 24px; white-space: pre-wrap; -webkit-box-align: center; align-items: center; position: relative;">content_copy 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)), );
</pre></aio-code></code-example>
在傳出最終字符串之前尖昏,
debounceTime(300)
將會等待仰税,直到新增字符串的事件暫停了 300 毫秒。 你實際發(fā)起請求的間隔永遠不會小于 300ms抽诉。distinctUntilChanged
會確保只在過濾條件變化時才發(fā)送請求陨簇。switchMap()
會為每個從debounce
和distinctUntilChanged
中通過的搜索詞調(diào)用搜索服務。 它會取消并丟棄以前的搜索可觀察對象迹淌,只保留最近的河绽。
借助 switchMap 操作符, 每個有效的擊鍵事件都會觸發(fā)一次 [HttpClient](https://angular.cn/api/common/http/HttpClient).get()
方法調(diào)用唉窃。 即使在每個請求之間都有至少 300ms 的間隔耙饰,仍然可能會同時存在多個尚未返回的 HTTP 請求。
switchMap()
會記住原始的請求順序纹份,只會返回最近一次 HTTP 方法調(diào)用的結(jié)果苟跪。 以前的那些請求都會被取消和舍棄。
注意蔓涧,取消前一個 searchHeroes()
可觀察對象并不會中止尚未完成的 HTTP 請求件已。 那些不想要的結(jié)果只會在它們抵達應用代碼之前被舍棄。
記住元暴,組件類中并沒有訂閱 heroes$
這個可觀察對象篷扩,而是由模板中的 AsyncPipe
完成的。
試試看
再次運行本應用茉盏。在這個 儀表盤 中鉴未,在搜索框中輸入一些文字。如果你輸入的字符匹配上了任何現(xiàn)有英雄的名字鸠姨,你將會看到如下效果:
<figure style="background: rgb(255, 255, 255); padding: 20px; display: inline-block; box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 5px 0px; margin: 0px 0px 14px; border-radius: 4px; color: rgba(0, 0, 0, 0.87); font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"></figure>