HTTP

获取和保存数据

在这一章中,我们将进行如下增强:

我们要让应用能够对远端服务器提供的Web API发起相应的HTTP调用。

当我们完成这一章时,应用会变成这样:在线例子 / 可下载的例子

延续上一步教程

前一章中,我们学会了在仪表盘和固定的英雄列表之间导航,并编辑选定的英雄。这也就是本章的起点。

保持应用的转译与运行

在终端窗口输入如下命令:

npm start

这个命令会在“监听”模式下运行TypeScript编译器,当代码变化时,它会自动重新编译。 同时,该命令还会在浏览器中启动该应用,并且当代码变化时刷新浏览器。

在后续构建《英雄指南》过程中,应用能持续运行,而不用中断服务来编译或刷新浏览器。

提供 HTTP 服务

Providing HTTP Services

HttpModule并不是 Angular 的核心模块。 它是 Angular 用来进行 Web 访问的一种可选方式,并位于一个名叫 @angular/http 的独立附属模块中,并作为 Angular 的 npm 包之一而发布出来。

systemjs.config中已经配置好了 SystemJS,并在必要时加载它,因此我们已经准备好从@angular/http中导入它了。

注册 HTTP 服务

我们的应用将会依赖于 Angular 的http服务,它本身又依赖于其它支持类服务。 来自@angular/http库中的HttpModule保存着这些 HTTP 相关服务提供商的全集。

我们要能从本应用的任何地方访问这些服务,就要把HttpModule添加到AppModuleimports列表中。 这里同时也是我们引导应用及其根组件AppComponent的地方。

src/app/app.module.ts (v1)

  1. import { NgModule } from '@angular/core';
  2. import { BrowserModule } from '@angular/platform-browser';
  3. import { FormsModule } from '@angular/forms';
  4. import { HttpModule } from '@angular/http';
  5. import { AppRoutingModule } from './app-routing.module';
  6. import { AppComponent } from './app.component';
  7. import { DashboardComponent } from './dashboard.component';
  8. import { HeroesComponent } from './heroes.component';
  9. import { HeroDetailComponent } from './hero-detail.component';
  10. import { HeroService } from './hero.service';
  11. @NgModule({
  12. imports: [
  13. BrowserModule,
  14. FormsModule,
  15. HttpModule,
  16. AppRoutingModule
  17. ],
  18. declarations: [
  19. AppComponent,
  20. DashboardComponent,
  21. HeroDetailComponent,
  22. HeroesComponent,
  23. ],
  24. providers: [ HeroService ],
  25. bootstrap: [ AppComponent ]
  26. })
  27. export class AppModule { }

注意,现在HttpModule已经是根模块AppModuleimports数组的一部分了。

模拟web API

我们建议在根模块AppModuleproviders数组中注册全应用级的服务。

在拥有一个能处理Web请求的服务器之前,我们可以先用HTTP客户端通过一个模拟(Mock)服务(内存Web API)来获取和保存数据。

修改src/app/app.module.ts,让它使用这个模拟服务:

src/app/app.module.ts (v2)

  1. import { NgModule } from '@angular/core';
  2. import { BrowserModule } from '@angular/platform-browser';
  3. import { FormsModule } from '@angular/forms';
  4. import { HttpModule } from '@angular/http';
  5. import { AppRoutingModule } from './app-routing.module';
  6. // Imports for loading & configuring the in-memory web api
  7. import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
  8. import { InMemoryDataService } from './in-memory-data.service';
  9. import { AppComponent } from './app.component';
  10. import { DashboardComponent } from './dashboard.component';
  11. import { HeroesComponent } from './heroes.component';
  12. import { HeroDetailComponent } from './hero-detail.component';
  13. import { HeroService } from './hero.service';
  14. @NgModule({
  15. imports: [
  16. BrowserModule,
  17. FormsModule,
  18. HttpModule,
  19. InMemoryWebApiModule.forRoot(InMemoryDataService),
  20. AppRoutingModule
  21. ],
  22. declarations: [
  23. AppComponent,
  24. DashboardComponent,
  25. HeroDetailComponent,
  26. HeroesComponent,
  27. ],
  28. providers: [ HeroService ],
  29. bootstrap: [ AppComponent ]
  30. })
  31. export class AppModule { }

导入InMemoryWebApiModule并将其加入到模块的imports数组。 InMemoryWebApiModuleHttp客户端默认的后端服务 — 这是一个辅助服务,负责与远程服务器对话 — 替换成了内存 Web API服务:

InMemoryWebApiModule.forRoot(InMemoryDataService),

forRoot()配置方法需要InMemoryDataService类实例,用来向内存数据库填充数据: 往app目录下新增一个文件in-memory-data.service.ts,填写下列内容:

src/app/in-memory-data.service.ts

import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
  createDb() {
    let heroes = [
      {id: 11, name: 'Mr. 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};
  }
}

这个文件已经替换了mock-heroes.ts,可以删除mock-heroes.ts了。

内存Web API只在开发的早期阶段或写《英雄指南》这样的演示程序时才有用。有了它,你将来替换后端实现时就不用关心这些细节问题了。如果你已经有了一个真实的Web API服务器,尽管跳过它吧。

关于内存 Web API 的更多信息,见 附录:英雄指南Web API中的HTTP 客户端部分。

英雄与 HTTP

在目前的HeroService的实现中,返回的是一个能解析(resolve)成模拟英雄列表的承诺(Promise)。

src/app/hero.service.ts (old getHeroes)

  1. getHeroes(): Promise<Hero[]> {
  2. return Promise.resolve(HEROES);
  3. }

我们返回一个承诺 (Promise),它用模拟版的英雄列表进行解析。 它当时可能看起来显得有点过于复杂,不过我们预料到总有这么一天会通过 HTTP 客户端来获取英雄数据, 而且我们知道,那一定是一个异步操作。

现在,我们把getHeroes()换成使用 HTTP。

src/app/hero.service.ts (updated getHeroes and new class members)

  1. private heroesUrl = 'api/heroes'; // URL to web api
  2. constructor(private http: Http) { }
  3. getHeroes(): Promise<Hero[]> {
  4. return this.http.get(this.heroesUrl)
  5. .toPromise()
  6. .then(response => response.json().data as Hero[])
  7. .catch(this.handleError);
  8. }
  9. private handleError(error: any): Promise<any> {
  10. console.error('An error occurred', error); // for demo purposes only
  11. return Promise.reject(error.message || error);
  12. }

更新后的导入声明如下:

src/app/hero.service.ts (updated imports)

  1. import { Injectable } from '@angular/core';
  2. import { Headers, Http } from '@angular/http';
  3. import 'rxjs/add/operator/toPromise';
  4. import { Hero } from './hero';

刷新浏览器后,英雄数据就会从模拟服务器被成功读取。

HTTP Promise

Angular 的http.get返回一个 RxJS 的Observable对象。 Observable(可观察对象)是一个管理异步数据流的强力方式。 后面我们还会进一步学习可观察对象

现在,我们先利用toPromise操作符把Observable直接转换成Promise对象,回到已经熟悉的地盘。

.toPromise()

不幸的是,Angular 的Observable并没有一个toPromise操作符... 没有打包在一起发布。Angular的Observable只是一个骨架实现。

有很多像toPromise这样的操作符,用于扩展Observable,为其添加有用的能力。 如果我们希望得到那些能力,就得自己添加那些操作符。 那很容易,只要从 RxJS 库中导入它们就可以了,就像这样:

import 'rxjs/add/operator/toPromise';

我们还要添加更多的操作符,并且必须这么做,要了解其中的原因,参见本章稍后的部分

then 回调中提取出数据

promisethen()回调中,我们调用 HTTP 的Reponse对象的json方法,以提取出其中的数据。

.then(response => response.json().data as Hero[])

这个由json方法返回的对象只有一个data属性。 这个data属性保存了英雄数组,这个数组才是调用者真正想要的。 所以我们取得这个数组,并且把它作为承诺的值进行解析。

仔细看看这个由服务器返回的数据的形态。 这个内存 Web API 的范例中所做的是返回一个带有data属性的对象。 你的 API 也可以返回其它东西。请调整这些代码以匹配你的 Web API

调用者并不知道这些实现机制,它仍然像以前那样接收一个包含英雄数据的承诺。 它也不知道我们已经改成了从服务器获取英雄数据。 它也不必了解把 HTTP 响应转换成英雄数据时所作的这些复杂变换。 看到美妙之处了吧,这正是将数据访问委托组一个服务的目的。

错误处理

getHeroes()的最后,我们catch了服务器的失败信息,并把它们传给了错误处理器:

.catch(this.handleError);

这是一个关键的步骤! 我们必须预料到 HTTP 请求会失败,因为有太多我们无法控制的原因可能导致它们频繁出现各种错误。

  1. private handleError(error: any): Promise<any> {
  2. console.error('An error occurred', error); // for demo purposes only
  3. return Promise.reject(error.message || error);
  4. }

在这个范例服务中,我们把错误记录到控制台中;在真实世界中,我们应该用代码对错误进行处理。但对于演示来说,这就够了。

我们还要通过一个被拒绝 (rejected) 的承诺来把该错误用一个用户友好的格式返回给调用者, 以便调用者能把一个合适的错误信息显示给用户。

通过id获取英雄

HeroDetailComponentHeroService请求获取一个英雄时,HeroService会获取所有英雄,并从中过滤出与id匹配的那一个。 这对于例子来说倒是无可厚非, 不过在真实服务中,这种为了获取一个英雄而请求全部英雄的做法就有点浪费了, 许多Web API支持get-by-id请求,形如:api/hero/:id(如:api/hero/11)。

修改 HeroService.getHero() 方法来发起一个 get-by-id 请求:

src/app/hero.service.ts

  1. getHero(id: number): Promise<Hero> {
  2. const url = `${this.heroesUrl}/${id}`;
  3. return this.http.get(url)
  4. .toPromise()
  5. .then(response => response.json().data as Hero)
  6. .catch(this.handleError);
  7. }

此方法基本上与getHeroes方法一致,通过在URL中添加英雄的id来告诉服务器应该获取那个英雄, 匹配api/hero/:id模式。

我们还要把响应中返回的data改为一个英雄对象,而不再是对象数组。组。

getHeroes API 没变

尽管我们在getHeroes()getHero()方法的内部做了重大修改, 但是他们的公共签名却没有变。这两个方法仍然返回的是一个Promise对象, 所以并不需要修改任何调用他们的组件。

现在,我们该支持创建和删除英雄了。

更新英雄详情

我们已经可以在英雄详情中编辑英雄的名字了。来试试吧。在输入的时候,页头上的英雄名字也会随之更新。 不过当我们点了Back(后退)按钮时,这些修改就丢失了。

以前是不会丢失更新的,怎么回事? 当该应用使用模拟出来的英雄列表时,修改的是一份全局共享的英雄列表,而现在改成了从服务器获取数据。 如果我们希望这些更改被持久化,我们就得把它们写回服务器。

保存英雄详情

我们先来确保对英雄名字的编辑不会丢失。先在英雄详情模板的底部添加一个保存按钮,它绑定了一个click事件,事件绑定会调用组件中一个名叫save()的新方法:

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

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

save()方法使用 hero 服务的update()方法来持久化对英雄名字的修改,然后导航回前一个视图:

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

  1. save(): void {
  2. this.heroService.update(this.hero)
  3. .then(() => this.goBack());
  4. }

hero 服务的update方法

update()方法的大致结构与getHeroes()类似,不过我们使用 HTTP 的 put() 方法来把修改持久化到服务端:

src/app/hero.service.ts (update)

  1. private headers = new Headers({'Content-Type': 'application/json'});
  2. update(hero: Hero): Promise<Hero> {
  3. const url = `${this.heroesUrl}/${hero.id}`;
  4. return this.http
  5. .put(url, JSON.stringify(hero), {headers: this.headers})
  6. .toPromise()
  7. .then(() => hero)
  8. .catch(this.handleError);
  9. }

我们通过一个编码在 URL 中的英雄 id 来告诉服务器应该更新哪个英雄。put 的 body 是该英雄的 JSON 字符串,它是通过调用JSON.stringify得到的。 并且在请求头中标记出的 body 的内容类型(application/json)。

刷新浏览器试一下,对英雄名字的修改确实已经被持久化了。

添加英雄

要添加一个新的英雄,我们得先知道英雄的名字。我们使用一个 input 元素和一个添加按钮来实现。

把下列代码插入 heroes 组件的 HTML 中,放在标题的下面:

src/app/heroes.component.html (add)

  1. <div>
  2. <label>Hero name:</label> <input #heroName />
  3. <button (click)="add(heroName.value); heroName.value=''">
  4. Add
  5. </button>
  6. </div>

当点击事件触发时,我们调用组件的点击处理器,然后清空这个输入框,以便用来输入另一个名字。

src/app/heroes.component.ts (add)

  1. add(name: string): void {
  2. name = name.trim();
  3. if (!name) { return; }
  4. this.heroService.create(name)
  5. .then(hero => {
  6. this.heroes.push(hero);
  7. this.selectedHero = null;
  8. });
  9. }

当指定的名字不为空的时候,点击处理器就会委托 hero 服务来创建一个具有此名字的英雄, 并把这个新的英雄添加到我们的数组中。

然后,我们在HeroService类中实现这个create()方法。

src/app/hero.service.ts (create)

  1. create(name: string): Promise<Hero> {
  2. return this.http
  3. .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers})
  4. .toPromise()
  5. .then(res => res.json().data as Hero)
  6. .catch(this.handleError);
  7. }

刷新浏览器,并创建一些新的英雄!

支持添加英雄

在英雄列表视图中的每个英雄都应该有一个删除按钮。

把这个按钮元素添加到英雄列表组件的 HTML 中,把它放在<li>标签中的英雄名的后面:

  1. <button class="delete"
  2. (click)="delete(hero); $event.stopPropagation()">x</button>

<li>元素应该变成了这样:

src/app/heroes.component.html (li-element)

  1. <li *ngFor="let hero of heroes" (click)="onSelect(hero)"
  2. [class.selected]="hero === selectedHero">
  3. <span class="badge">{{hero.id}}</span>
  4. <span>{{hero.name}}</span>
  5. <button class="delete"
  6. (click)="delete(hero); $event.stopPropagation()">x</button>
  7. </li>

除了调用组件的delete()方法之外,这个删除按钮的点击处理器还应该阻止点击事件向上冒泡 — 我们并不希望触发<li>的事件处理器,否则它会选中我们要删除的这位英雄。

delete()处理器的逻辑略复杂:

src/app/heroes.component.ts (delete)

  1. delete(hero: Hero): void {
  2. this.heroService
  3. .delete(hero.id)
  4. .then(() => {
  5. this.heroes = this.heroes.filter(h => h !== hero);
  6. if (this.selectedHero === hero) { this.selectedHero = null; }
  7. });
  8. }

当然,我们仍然把删除英雄的操作委托给了 hero 服务, 不过该组件仍然负责更新显示:它从数组中移除了被删除的英雄,如果删除的是正选中的英雄,还会清空选择。

我们希望删除按钮被放在英雄条目的最右边。 于是 CSS 变成了这样:

src/app/heroes.component.css (additions)

  1. button.delete {
  2. float:right;
  3. margin-top: 2px;
  4. margin-right: .8em;
  5. background-color: gray !important;
  6. color:white;
  7. }

hero 服务的delete()方法

hero 服务的delete()方法使用 HTTP 的 delete() 方法来从服务器上移除该英雄:

src/app/hero.service.ts (delete)

  1. delete(id: number): Promise<void> {
  2. const url = `${this.heroesUrl}/${id}`;
  3. return this.http.delete(url, {headers: this.headers})
  4. .toPromise()
  5. .then(() => null)
  6. .catch(this.handleError);
  7. }

刷新浏览器,并试一下这个新的删除功能。

可观察对象 (Observable)

Http服务中的每个方法都返回一个 HTTP Response对象的Observable实例。

我们的HeroService中把那个Observable对象转换成了Promise(承诺),并把这个承诺返回给了调用者。 这一节,我们将学会直接返回Observable,并且讨论何时以及为何那样做会更好。

背景

一个可观察对象是一个事件流,我们可以用数组型操作符来处理它。

Angular 内核中提供了对可观察对象的基本支持。而我们这些开发人员可以自己从 RxJS 库中引入操作符和扩展。 我们会简短的讲解下如何做。

快速回忆一下HeroService,它在http.get()返回的Observable后面串联了一个toPromise操作符。 该操作符把Observable转换成了Promise,并且我们把那个承诺返回给了调用者。

转换成承诺通常是更好地选择,我们通常会要求http.get()获取单块数据。只要接收到数据,就算完成。 使用承诺这种形式的结果是让调用方更容易写,并且承诺已经在 JavaScript 程序员中被广泛接受了。

但是请求并非总是“一次性”的。我们可以开始一个请求, 并且取消它,在服务器对第一个请求作出回应之前,再开始另一个不同的请求 。 像这样一个请求-取消-新请求的序列用承诺是很难实现的,但接下来我们会看到,它对于可观察对象却很简单。

请求-取消-新请求的序列对于Promise来说是很难实现的,但是对Observable来说则很容易。

支持按名搜索

我们要为《英雄指南》添加一个英雄搜索特性。 当用户在搜索框中输入一个名字时,我们将不断发起 HTTP 请求,以获得按名字过滤的英雄。

我们先创建HeroSearchService服务,它会把搜索请求发送到我们服务器上的 Web API。

src/app/hero-search.service.ts

  1. import { Injectable } from '@angular/core';
  2. import { Http } from '@angular/http';
  3. import { Observable } from 'rxjs/Observable';
  4. import 'rxjs/add/operator/map';
  5. import { Hero } from './hero';
  6. @Injectable()
  7. export class HeroSearchService {
  8. constructor(private http: Http) {}
  9. search(term: string): Observable<Hero[]> {
  10. return this.http
  11. .get(`app/heroes/?name=${term}`)
  12. .map(response => response.json().data as Hero[]);
  13. }
  14. }

HeroSearchService中的http.get()调用和HeroService中的很相似,只是这次带了查询字符串。

更重要的是:我们不再调用toPromise方法,而是从http.get 方法中返回一个Observable对象,之后调用RxJS的map操作符 来从返回数据中提取英雄。

链式RxJS操作可以让我们简单、易读的处理响应数据。详见下面关于操作符的讨论

HeroSearchComponent

我们再创建一个新的HeroSearchComponent来调用这个新的HeroSearchService

组件模板很简单,就是一个输入框和一个显示匹配的搜索结果的列表。

src/app/hero-search.component.html

  1. <div id="search-component">
  2. <h4>Hero Search</h4>
  3. <input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
  4. <div>
  5. <div *ngFor="let hero of heroes | async"
  6. (click)="gotoDetail(hero)" class="search-result" >
  7. {{hero.name}}
  8. </div>
  9. </div>
  10. </div>

我们还要往这个新组件中添加样式。

src/app/hero-search.component.css

  1. .search-result{
  2. border-bottom: 1px solid gray;
  3. border-left: 1px solid gray;
  4. border-right: 1px solid gray;
  5. width:195px;
  6. height: 16px;
  7. padding: 5px;
  8. background-color: white;
  9. cursor: pointer;
  10. }
  11. .search-result:hover {
  12. color: #eee;
  13. background-color: #607D8B;
  14. }
  15. #search-box{
  16. width: 200px;
  17. height: 20px;
  18. }

当用户在搜索框中输入时,一个 keyup 事件绑定会调用该组件的search()方法,并传入新的搜索框的值。

不出所料,*ngFor从该组件的heroes属性重复获取 hero 对象。这也没啥特别的。

但是,接下来我们看到heroes属性现在是英雄列表的Observable对象,而不再只是英雄数组。 *ngFor不能用可观察对象做任何事,除非我们在它后面跟一个async pipe (AsyncPipe)。 这个async管道会订阅到这个可观察对象,并且为*ngFor生成一个英雄数组。

该创建HeroSearchComponent类及其元数据了。

src/app/hero-search.component.ts

  1. import { Component, OnInit } from '@angular/core';
  2. import { Router } from '@angular/router';
  3. import { Observable } from 'rxjs/Observable';
  4. import { Subject } from 'rxjs/Subject';
  5. // Observable class extensions
  6. import 'rxjs/add/observable/of';
  7. // Observable operators
  8. import 'rxjs/add/operator/catch';
  9. import 'rxjs/add/operator/debounceTime';
  10. import 'rxjs/add/operator/distinctUntilChanged';
  11. import { HeroSearchService } from './hero-search.service';
  12. import { Hero } from './hero';
  13. @Component({
  14. selector: 'hero-search',
  15. templateUrl: './hero-search.component.html',
  16. styleUrls: [ './hero-search.component.css' ],
  17. providers: [HeroSearchService]
  18. })
  19. export class HeroSearchComponent implements OnInit {
  20. heroes: Observable<Hero[]>;
  21. private searchTerms = new Subject<string>();
  22. constructor(
  23. private heroSearchService: HeroSearchService,
  24. private router: Router) {}
  25. // Push a search term into the observable stream.
  26. search(term: string): void {
  27. this.searchTerms.next(term);
  28. }
  29. ngOnInit(): void {
  30. this.heroes = this.searchTerms
  31. .debounceTime(300) // wait 300ms after each keystroke before considering the term
  32. .distinctUntilChanged() // ignore if next search term is same as previous
  33. .switchMap(term => term // switch to new observable each time the term changes
  34. // return the http search observable
  35. ? this.heroSearchService.search(term)
  36. // or the observable of empty heroes if there was no search term
  37. : Observable.of<Hero[]>([]))
  38. .catch(error => {
  39. // TODO: add real error handling
  40. console.log(error);
  41. return Observable.of<Hero[]>([]);
  42. });
  43. }
  44. gotoDetail(hero: Hero): void {
  45. let link = ['/detail', hero.id];
  46. this.router.navigate(link);
  47. }
  48. }

搜索词

仔细看下这个searchTerms

  1. private searchTerms = new Subject<string>();
  2. // Push a search term into the observable stream.
  3. search(term: string): void {
  4. this.searchTerms.next(term);
  5. }

Subject(主题)是一个可观察的事件流中的生产者。 searchTerms生成一个产生字符串的Observable,用作按名称搜索时的过滤条件。Each call to search() puts a new string into this subject's observable stream by calling next().

每当调用search()时都会调用next()来把新的字符串放进该主题的可观察流中。

初始化 heroes 属性(ngOnInit)

Subject也是一个Observable对象。 我们要把搜索词的流转换成Hero数组的流,并把结果赋值给heroes属性。

  1. heroes: Observable<Hero[]>;
  2. ngOnInit(): void {
  3. this.heroes = this.searchTerms
  4. .debounceTime(300) // wait 300ms after each keystroke before considering the term
  5. .distinctUntilChanged() // ignore if next search term is same as previous
  6. .switchMap(term => term // switch to new observable each time the term changes
  7. // return the http search observable
  8. ? this.heroSearchService.search(term)
  9. // or the observable of empty heroes if there was no search term
  10. : Observable.of<Hero[]>([]))
  11. .catch(error => {
  12. // TODO: add real error handling
  13. console.log(error);
  14. return Observable.of<Hero[]>([]);
  15. });
  16. }

如果我们直接把每一次用户按键都直接传给HeroSearchService,就会发起一场 HTTP 请求风暴。 这可不好玩。我们不希望占用服务器资源,也不想耗光蜂窝移动网络的流量。

不过,我们可以在字符串的Observable后面串联一些Observable操作符,来归并这些请求。 我们将对HeroSearchService发起更少的调用,并且仍然获得足够及时的响应。做法如下:

借助switchMap操作符 (正式名称是flatMapLatest) 每次符合条件的按键事件都会触发一次对http()方法的调用。即使在发送每个请求前都有 300 毫秒的延迟, 我们仍然可能同时拥有多个在途的 HTTP 请求,并且它们返回的顺序未必就是发送时的顺序。

switchMap()保留了原始的请求顺序,并且只返回最近一次 http 调用返回的可观察对象。 这是因为以前的调用都被取消或丢弃了。

如果搜索框为空,我们还可以短路掉这次http()方法调用,并且直接返回一个包含空数组的可观察对象。

注意,取消HeroSearchService的可观察对象并不会实际中止 (abort) 一个未完成的 HTTP 请求, 除非服务支持这个特性,这个问题我们以后再讨论。 目前我们的做法只是丢弃不希望的结果。

导入 RxJS 操作符

大部分RxJS操作符都不包括在Angular的Observable基本实现中,基本实现只包括Angular本身所需的功能。

如果想要更多的RxJS功能,我们必须导入其所定义的库来扩展Observable对象, 以下是这个模块所需导入的所有RxJS操作符:

src/app/hero-search.component.ts (rxjs imports)

import { Observable }        from 'rxjs/Observable';
import { Subject }           from 'rxjs/Subject';

// Observable class extensions
import 'rxjs/add/observable/of';

// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';

你可能并不熟悉这种import 'rxjs/add/...'语法,它缺少了花括号中的导入列表:{...}

这是因为我们并不需要操作符本身,这种情况下,我们所做的其实是导入这个库,加载并运行其中的脚本, 它会把操作符添加到Observable类中。

为仪表盘添加搜索组件

将表示“英雄搜索”组件的 HTML 元素添加到DashboardComponent模版的最后面。

src/app/dashboard.component.html

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

最后,从hero-search.component.ts中导入HeroSearchComponent并将其添加到declarations数组中。

src/app/app.module.ts (search)

  1. declarations: [
  2. AppComponent,
  3. DashboardComponent,
  4. HeroDetailComponent,
  5. HeroesComponent,
  6. HeroSearchComponent
  7. ],

再次运行该应用,跳转到仪表盘,并在英雄下方的搜索框里输入一些文本。 运行效果如下:

Hero Search Component

应用的结构与代码

回顾一下本章在线例子 / 可下载的例子中的范例代码。 验证我们是否得到了如下结构:

angular-tour-of-heroes
src
app
app.component.ts
app.component.css
app.module.ts
app-routing.module.ts
dashboard.component.css
dashboard.component.html
dashboard.component.ts
hero.ts
hero-detail.component.css
hero-detail.component.html
hero-detail.component.ts
hero-search.component.html (new)
hero-search.component.css (new)
hero-search.component.ts (new)
hero-search.service.ts (new)
hero.service.ts
heroes.component.css
heroes.component.html
heroes.component.ts
in-memory-data.service.ts (new)
main.ts
index.html
styles.css
systemjs.config.js
tsconfig.json
node_modules ...
package.json

最后冲刺

旅程即将结束,不过我们已经收获颇丰。

下面是我们添加或修改之后的文件汇总。

  1. import { Component } from '@angular/core';
  2. @Component({
  3. selector: 'my-app',
  4. template: `
  5. <h1>{{title}}</h1>
  6. <nav>
  7. <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
  8. <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
  9. </nav>
  10. <router-outlet></router-outlet>
  11. `,
  12. styleUrls: ['./app.component.css']
  13. })
  14. export class AppComponent {
  15. title = 'Tour of Heroes';
  16. }
  1. import { Injectable } from '@angular/core';
  2. import { Http } from '@angular/http';
  3. import { Observable } from 'rxjs/Observable';
  4. import 'rxjs/add/operator/map';
  5. import { Hero } from './hero';
  6. @Injectable()
  7. export class HeroSearchService {
  8. constructor(private http: Http) {}
  9. search(term: string): Observable<Hero[]> {
  10. return this.http
  11. .get(`app/heroes/?name=${term}`)
  12. .map(response => response.json().data as Hero[]);
  13. }
  14. }

下一步

返回学习路径,你可以阅读在本教程中探索到的概念和实践。