HTTP

获取和保存数据

Our stakeholders appreciate our progress. Now they want to get the hero data from a server, let users add, edit, and delete heroes, and save these changes back to the server.

客户对我们的进展很满意! 现在,它们想要从服务器获取英雄数据,然后让用户添加、编辑和删除英雄,并且把这些修改结果保存回服务器。

In this chapter we teach our application to make the corresponding HTTP calls to a remote server's web API.

在这一章中,我们要让应用程序通过 HTTP 调用来访问远程服务器上相应的 Web API。

Run the for this part.

运行这部分的在线例子

Where We Left Off

延续上一步教程

In the previous chapter, we learned to navigate between the dashboard and the fixed heroes list, editing a selected hero along the way. That's our starting point for this chapter.

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

Keep the app transpiling and running

保持应用的转译与运行

Open a terminal/console window and enter the following command to start the TypeScript compiler, start the server, and watch for changes:

打开终端/控制台窗口,输入下列命令来启动 TypeScript 编译器,它会启动开发服务器,并监视文件变更:

npm start

The application runs and updates automatically as we continue to build the Tour of Heroes.

当我们继续构建《英雄指南》时,应用会运行并自动更新。

Providing HTTP Services

准备 HTTP 服务

The HttpModule is not a core Angular module. It's Angular's optional approach to web access and it exists as a separate add-on module called @angular/http, shipped in a separate script file as part of the Angular npm package.

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

Fortunately we're ready to import from @angular/http because systemjs.config configured SystemJS to load that library when we need it.

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

Register for HTTP services

注册 HTTP 服务

Our app will depend upon the Angular http service which itself depends upon other supporting services. The HttpModule from @angular/http library holds providers for a complete set of HTTP services.

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

We should be able to access these services from anywhere in the application. So we register them all by adding HttpModule to the imports list of the AppModule where we bootstrap the application and its root AppComponent.

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

app/app.module.ts (v1)

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { DashboardComponent } from './dashboard.component'; import { HeroesComponent } from './heroes.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroService } from './hero.service'; @NgModule({ imports: [ BrowserModule, FormsModule, HttpModule, AppRoutingModule ], declarations: [ AppComponent, DashboardComponent, HeroDetailComponent, HeroesComponent, ], providers: [ HeroService ], bootstrap: [ AppComponent ] }) export class AppModule { }

Notice that we supply HttpModule as part of the imports array in root NgModule AppModule.

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

Simulating the web API

模拟web API

We recommend registering application-wide services in the root AppModule providers. Here we're registering in main for a special reason.

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

Our application is in the early stages of development and far from ready for production. We don't even have a web server that can handle requests for heroes. Until we do, we'll have to fake it.

我们的应用正处于开发的早期阶段,并且离进入产品阶段还很远。 我们甚至都还没有一个用来处理英雄相关请求的 Web 服务器,在此之前,我们将不得不伪造一个

We're going to trick the HTTP client into fetching and saving data from a mock service, the in-memory web API. The application itself doesn't need to know and shouldn't know about this. So we'll slip the in-memory web API into the configuration above the AppComponent.

我们要耍点小花招,让 HTTP 客户端从一个模拟服务(内存 Web API)中获取和保存数据。

Here is a version of app/app.module.ts that performs this trick:

这个版本的app/app.module.ts就是用来实现这个小花招的:

app/app.module.ts (v2)

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppRoutingModule } from './app-routing.module'; // Imports for loading & configuring the in-memory web api import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; import { InMemoryDataService } from './in-memory-data.service'; import { AppComponent } from './app.component'; import { DashboardComponent } from './dashboard.component'; import { HeroesComponent } from './heroes.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroService } from './hero.service'; @NgModule({ imports: [ BrowserModule, FormsModule, HttpModule, InMemoryWebApiModule.forRoot(InMemoryDataService), AppRoutingModule ], declarations: [ AppComponent, DashboardComponent, HeroDetailComponent, HeroesComponent, ], providers: [ HeroService ], bootstrap: [ AppComponent ] }) export class AppModule { }

We're importing the InMemoryWebApiModule and adding it to the module imports. The InMemoryWebApiModule replaces the default Http client backend — the supporting service that talks to the remote server — with an in-memory web API alternative service.

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

InMemoryWebApiModule.forRoot(InMemoryDataService),

The forRoot configuration method takes an InMemoryDataService class that primes the in-memory database as follows:

forRoot配置方法需要InMemoryDataService类实例,用来向内存数据库填充数据:

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

This file replaces the mock-heroes.ts which is now safe to delete.

该文件代替了mock-heroes.ts文件,它现在可以安全的删除了。

This chapter is an introduction to the Angular HTTP library. Please don't be distracted by the details of this backend substitution. Just follow along with the example.

本章主要是介绍 Angular 的 HTTP 库,不要因为这种“替换后端”的细节而分心。 先不要管为什么,只管照着这个例子做就可以了。

Learn more later about the in-memory web API in the HTTP client chapter. Remember, the in-memory web API is only useful in the early stages of development and for demonstrations such as this Tour of Heroes. Skip it when you have a real web API server.

关于内存 Web API 的更多信息,见 HTTP 客户端。 记住,内存 Web API 主要用于开发的早期阶段或《英雄指南》这样的演示程序。 如果你已经有了一个真实的 Web API 服务器,尽管跳过它。

Heroes and HTTP

英雄与 HTTP

Look at our current HeroService implementation

来看看我们目前的HeroService的实现

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

We returned a Promise resolved with mock heroes. It may have seemed like overkill at the time, but we were anticipating the day when we fetched heroes with an HTTP client and we knew that would have to be an asynchronous operation.

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

That day has arrived! Let's convert getHeroes() to use HTTP.

这一天到来了!我们把getHeroes()换成用 HTTP。

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

private heroesUrl = 'app/heroes'; // URL to web api constructor(private http: Http) { } getHeroes(): Promise<Hero[]> { return this.http.get(this.heroesUrl) .toPromise() .then(response => response.json().data as Hero[]) .catch(this.handleError); }

Our updated import statements are now:

更新后的导入声明如下:

app/hero.service.ts (updated imports)

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

Refresh the browser, and the hero data should be successfully loaded from the mock server.

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

HTTP Promise

Http承诺(Promise)

We're still returning a Promise but we're creating it differently.

我们仍然返回一个承诺,但是用不同的方法来创建它。

The Angular http.get returns an RxJS Observable. Observables are a powerful way to manage asynchronous data flows. We'll learn about Observables later in this chapter.

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

For now we get back on familiar ground by immediately converting that Observable to a Promise using the toPromise operator.

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

.toPromise()

Unfortunately, the Angular Observable doesn't have a toPromise operator ... not out of the box. The Angular Observable is a bare-bones implementation.

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

There are scores of operators like toPromise that extend Observable with useful capabilities. If we want those capabilities, we have to add the operators ourselves. That's as easy as importing them from the RxJS library like this:

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

import 'rxjs/add/operator/toPromise';

Extracting the data in the then callback

then 回调中提取出数据

In the promise's then callback we call the json method of the HTTP Response to extract the data within the response.

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

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

That response JSON has a single data property. The data property holds the array of heroes that the caller really wants. So we grab that array and return it as the resolved Promise value.

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

Pay close attention to the shape of the data returned by the server. This particular in-memory web API example happens to return an object with a data property. Your API might return something else. Adjust the code to match your web API.

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

The caller is unaware of these machinations. It receives a Promise of heroes just as it did before. It has no idea that we fetched the heroes from the (mock) server. It knows nothing of the twists and turns required to convert the HTTP response into heroes. Such is the beauty and purpose of delegating data access to a service like this HeroService.

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

Error Handling

错误处理

At the end of getHeroes() we catch server failures and pass them to an error handler:

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

.catch(this.handleError);

This is a critical step! We must anticipate HTTP failures as they happen frequently for reasons beyond our control.

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

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

In this demo service we log the error to the console; we would do better in real life.

在这个范例服务中,我们把错误记录到控制台中;在真实世界中,我们应该做得更好。

We've also decided to return a user friendly form of the error to the caller in a rejected promise so that the caller can display a proper error message to the user.

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

Unchanged getHeroes API

getHeroes API 没变

Although we made significant internal changes to getHeroes(), the public signature did not change. We still return a Promise. We won't have to update any of the components that call getHeroes().

虽然我们对getHeroes()做了一些重要的内部修改,该方法公开的函数签名却没有任何变化。 我们返回的仍然是一个承诺,不用被迫更新任何一个调用了getHeroes()的组件。

Our stakeholders are thrilled with the added flexibility from the API integration. Now they want the ability to create and delete heroes.

我们的客户很欣赏这种富有弹性的 API 集成方式。 现在它们想增加创建和删除英雄的功能。

Let's see first what happens when we try to update a hero's details.

我们来看看在更新英雄的详情时会发生什么。

Update hero details

更新英雄详情

We can edit a hero's name already in the hero detail view. Go ahead and try it. As we type, the hero name is updated in the view heading. But when we hit the Back button, the changes are lost!

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

Updates weren't lost before, what's happening? When the app used a list of mock heroes, changes were made directly to the hero objects in the single, app-wide shared list. Now that we are fetching data from a server, if we want changes to persist, we'll need to write them back to the server.

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

Save hero details

保存英雄详情

Let's ensure that edits to a hero's name aren't lost. Start by adding, to the end of the hero detail template, a save button with a click event binding that invokes a new component method named save:

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

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

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

The save method persists hero name changes using the hero service update method and then navigates back to the previous view:

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

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

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

Hero service update method

hero 服务的update方法

The overall structure of the update method is similar to that of getHeroes, although we'll use an HTTP put to persist changes server-side:

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

app/hero.service.ts (update)

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

We identify which hero the server should update by encoding the hero id in the URL. The put body is the JSON string encoding of the hero, obtained by calling JSON.stringify. We identify the body content type (application/json) in the request header.

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

Refresh the browser and give it a try. Changes to hero names should now persist.

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

Add a hero

添加英雄

To add a new hero we need to know the hero's name. Let's use an input element for that, paired with an add button.

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

Insert the following into the heroes component HTML, first thing after the heading:

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

app/heroes.component.html (add)

<div> <label>Hero name:</label> <input #heroName /> <button (click)="add(heroName.value); heroName.value=''"> Add </button> </div>

In response to a click event, we call the component's click handler and then clear the input field so that it will be ready to use for another name.

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

app/heroes.component.ts (add)

add(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.create(name) .then(hero => { this.heroes.push(hero); this.selectedHero = null; }); }

When the given name is non-blank, the handler delegates creation of the named hero to the hero service, and then adds the new hero to our array.

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

Finally, we implement the create method in the HeroService class.

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

app/hero.service.ts (create)

create(name: string): Promise<Hero> { return this.http .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers}) .toPromise() .then(res => res.json().data) .catch(this.handleError); }

Refresh the browser and create some new heroes!

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

Delete a hero

删除一个英雄

Too many heroes? Let's add a delete button to each hero in the heroes view.

英雄太多了? 我们在英雄列表视图中来为每个英雄添加一个删除按钮吧。

Add this button element to the heroes component HTML, right after the hero name in the repeated <li> tag:

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

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

The <li> element should now look like this:

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

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

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

In addition to calling the component's delete method, the delete button click handling code stops the propagation of the click event — we don't want the <li> click handler to be triggered because that would select the hero that we are going to delete!

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

The logic of the delete handler is a bit trickier:

delete处理器的逻辑略复杂:

app/heroes.component.ts (delete)

delete(hero: Hero): void { this.heroService .delete(hero.id) .then(() => { this.heroes = this.heroes.filter(h => h !== hero); if (this.selectedHero === hero) { this.selectedHero = null; } }); }

Of course, we delegate hero deletion to the hero service, but the component is still responsible for updating the display: it removes the deleted hero from the array and resets the selected hero if necessary.

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

We want our delete button to be placed at the far right of the hero entry. This extra CSS accomplishes that:

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

app/heroes.component.css (additions)

button.delete { float:right; margin-top: 2px; margin-right: .8em; background-color: gray !important; color:white; }

Hero service delete method

hero 服务的delete方法

The hero service's delete method uses the delete HTTP method to remove the hero from the server:

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

app/hero.service.ts (delete)

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

Refresh the browser and try the new delete functionality.

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

Observables

可观察对象 (Observable)

Each Http service method returns an Observable of HTTP Response objects.

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

Our HeroService converts that Observable into a Promise and returns the promise to the caller. In this section we learn to return the Observable directly and discuss when and why that might be a good thing to do.

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

Background

背景

An observable is a stream of events that we can process with array-like operators.

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

Angular core has basic support for observables. We developers augment that support with operators and extensions from the RxJS Observables library. We'll see how shortly.

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

Recall that our HeroService quickly chained the toPromise operator to the Observable result of http.get. That operator converted the Observable into a Promise and we passed that promise back to the caller.

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

Converting to a promise is often a good choice. We typically ask http.get to fetch a single chunk of data. When we receive the data, we're done. A single result in the form of a promise is easy for the calling component to consume and it helps that promises are widely understood by JavaScript programmers.

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

But requests aren't always "one and done". We may start one request, then cancel it, and make a different request before the server has responded to the first request. Such a request-cancel-new-request sequence is difficult to implement with Promises. It's easy with Observables as we'll see.

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

Search-by-name

按名搜索

We're going to add a hero search feature to the Tour of Heroes. As the user types a name into a search box, we'll make repeated HTTP requests for heroes filtered by that name.

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

We start by creating HeroSearchService that sends search queries to our server's web api.

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

app/hero-search.service.ts

import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs'; import { Hero } from './hero'; @Injectable() export class HeroSearchService { constructor(private http: Http) {} search(term: string): Observable<Hero[]> { return this.http .get(`app/heroes/?name=${term}`) .map((r: Response) => r.json().data as Hero[]); } }

The http.get() call in HeroSearchService is similar to the one in the HeroService, although the URL now has a query string. Another notable difference: we no longer call toPromise, we simply return the observable instead.

HeroSearchService中的http.get()调用和HeroService中的很相似,只是这次带了查询字符串。 显著的不同是:我们不再调用toPromise,而是直接返回可观察对象

HeroSearchComponent

HeroSearchComponent

Let's create a new HeroSearchComponent that calls this new HeroSearchService.

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

The component template is simple — just a text box and a list of matching search results.

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

app/hero-search.component.html

<div id="search-component"> <h4>Hero Search</h4> <input #searchBox id="search-box" (keyup)="search(searchBox.value)" /> <div> <div *ngFor="let hero of heroes | async" (click)="gotoDetail(hero)" class="search-result" > {{hero.name}} </div> </div> </div>

We'll also want to add styles for the new component.

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

app/hero-search.component.css

.search-result{ border-bottom: 1px solid gray; border-left: 1px solid gray; border-right: 1px solid gray; width:195px; height: 20px; padding: 5px; background-color: white; cursor: pointer; } #search-box{ width: 200px; height: 20px; }

As the user types in the search box, a keyup event binding calls the component's search method with the new search box value.

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

The *ngFor repeats hero objects from the component's heroes property. No surprise there.

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

But, as we'll soon see, the heroes property is now an Observable of hero arrays, rather than just a hero array. The *ngFor can't do anything with an Observable until we flow it through the async pipe (AsyncPipe). The async pipe subscribes to the Observable and produces the array of heroes to *ngFor.

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

Time to create the HeroSearchComponent class and metadata.

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

app/hero-search.component.ts

import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; import { HeroSearchService } from './hero-search.service'; import { Hero } from './hero'; @Component({ moduleId: module.id, selector: 'hero-search', templateUrl: 'hero-search.component.html', styleUrls: [ 'hero-search.component.css' ], providers: [HeroSearchService] }) export class HeroSearchComponent implements OnInit { heroes: Observable<Hero[]>; private searchTerms = new Subject<string>(); constructor( private heroSearchService: HeroSearchService, private router: Router) {} // Push a search term into the observable stream. search(term: string): void { this.searchTerms.next(term); } ngOnInit(): void { this.heroes = this.searchTerms .debounceTime(300) // wait for 300ms pause in events .distinctUntilChanged() // ignore if next search term is same as previous .switchMap(term => term // switch to new observable each time // return the http search observable ? this.heroSearchService.search(term) // or the observable of empty heroes if no search term : Observable.of<Hero[]>([])) .catch(error => { // TODO: real error handling console.log(error); return Observable.of<Hero[]>([]); }); } gotoDetail(hero: Hero): void { let link = ['/detail', hero.id]; this.router.navigate(link); } }

Search terms

搜索词

Let's focus on the searchTerms:

仔细看下这个searchTerms

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

A Subject is a producer of an observable event stream; searchTerms produces an Observable of strings, the filter criteria for the name search.

Subject(主题)是一个可观察的事件流中的生产者。 searchTerms生成一个产生字符串的Observable,用作按名称搜索时的过滤条件。

Each call to search puts a new string into this subject's observable stream by calling next.

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

Initialize the heroes property (ngOnInit)

初始化 heroes 属性(ngOnInit)

A Subject is also an Observable. We're going to turn the stream of search terms into a stream of Hero arrays and assign the result to the heroes property.

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

heroes: Observable<Hero[]>; ngOnInit(): void { this.heroes = this.searchTerms .debounceTime(300) // wait for 300ms pause in events .distinctUntilChanged() // ignore if next search term is same as previous .switchMap(term => term // switch to new observable each time // return the http search observable ? this.heroSearchService.search(term) // or the observable of empty heroes if no search term : Observable.of<Hero[]>([])) .catch(error => { // TODO: real error handling console.log(error); return Observable.of<Hero[]>([]); }); }

If we passed every user keystroke directly to the HeroSearchService, we'd unleash a storm of HTTP requests. Bad idea. We don't want to tax our server resources and burn through our cellular network data plan.

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

Fortunately, we can chain Observable operators to the string Observable that reduce the request flow. We'll make fewer calls to the HeroSearchService and still get timely results. Here's how:

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

The switchMap operator (formerly known as "flatMapLatest") is very clever.

switchMap操作符 (以前叫"flatMapLatest")是非常智能的。

Every qualifying key event can trigger an http method call. Even with a 300ms pause between requests, we could have multiple HTTP requests in flight and they may not return in the order sent.

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

switchMap preserves the original request order while returning only the observable from the most recent http method call. Results from prior calls are canceled and discarded.

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

We also short-circuit the http method call and return an observable containing an empty array if the search text is empty.

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

Note that canceling the HeroSearchService observable won't actually abort a pending HTTP request until the service supports that feature, a topic for another day. We are content for now to discard unwanted results.

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

Import RxJS operators

导入 RxJS 操作符

The RxJS operators are not available in Angular's base Observable implementation. We have to extend Observable by importing them.

Angular 的基本版Observable实现中,RxJS 操作符是不可用的。 我们得导入它们,以扩展Observable

We could extend Observable with just the operators we need here by including the pertinent import statements at the top of this file.

通过在本文件的顶部写上适当的import语句, 我们可以为Observable扩展出这里用到的那些操作符。

Many authorities say we should do just that.

有很多权威人士建议我们这样做。

We take a different approach in this example. We combine all of the RxJS Observable extensions that our entire app requires into a single RxJS imports file.

在这个例子中,我们使用一些不同的方法。 我们把整个应用中要用的那些 RxJS Observable扩展组合在一起,放在一个单独的 RxJS 导入文件中。

app/rxjs-extensions.ts

// Observable class extensions import 'rxjs/add/observable/of'; import 'rxjs/add/observable/throw'; // Observable operators import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/filter'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/switchMap';

We load them all at once by importing rxjs-extensions at the top of AppModule.

我们在顶级的AppModule中导入rxjs-extensions就可以一次性加载它们。

app/app.module.ts (rxjs-extensions)

import './rxjs-extensions';

Add the search component to the dashboard

为仪表盘添加搜索组件

We add the hero search HTML element to the bottom of the DashboardComponent template.

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

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>

Finally, we import HeroSearchComponent from hero-search.component.ts and add it to the declarations array:

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

app/app.module.ts (search)

declarations: [ AppComponent, DashboardComponent, HeroDetailComponent, HeroesComponent, HeroSearchComponent ],

Run the app again, go to the Dashboard, and enter some text in the search box. At some point it might look like this.

再次运行该应用,跳转到仪表盘,并在英雄下方的搜索框里输入一些文本。 看起来就像这样:

Hero Search Component

Application structure and code

应用的结构与代码

Review the sample source code in the for this chapter. Verify that we have the following structure:

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

angular-tour-of-heroes
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)
rxjs-extensions.ts
hero.service.ts
heroes.component.css
heroes.component.html
heroes.component.ts
main.ts
in-memory-data.service.ts (new)
node_modules ...
index.html
package.json
styles.css
systemjs.config.js
tsconfig.json

Home Stretch

最后冲刺

We are at the end of our journey for now, but we have accomplished a lot.

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

Here are the files we added or changed in this chapter.

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

import { Component } from '@angular/core'; @Component({ moduleId: module.id, selector: 'my-app', template: ` <h1>{{title}}</h1> <nav> <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <router-outlet></router-outlet> `, styleUrls: ['app.component.css'] }) export class AppComponent { title = 'Tour of Heroes'; } import './rxjs-extensions'; import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppRoutingModule } from './app-routing.module'; // Imports for loading & configuring the in-memory web api import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; import { InMemoryDataService } from './in-memory-data.service'; import { AppComponent } from './app.component'; import { DashboardComponent } from './dashboard.component'; import { HeroesComponent } from './heroes.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroService } from './hero.service'; import { HeroSearchComponent } from './hero-search.component'; @NgModule({ imports: [ BrowserModule, FormsModule, HttpModule, InMemoryWebApiModule.forRoot(InMemoryDataService), AppRoutingModule ], declarations: [ AppComponent, DashboardComponent, HeroDetailComponent, HeroesComponent, HeroSearchComponent ], providers: [ HeroService ], bootstrap: [ AppComponent ] }) export class AppModule { } import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Hero } from './hero'; import { HeroService } from './hero.service'; @Component({ moduleId: module.id, selector: 'my-heroes', templateUrl: 'heroes.component.html', styleUrls: [ 'heroes.component.css' ] }) export class HeroesComponent implements OnInit { heroes: Hero[]; selectedHero: Hero; constructor( private heroService: HeroService, private router: Router) { } getHeroes(): void { this.heroService .getHeroes() .then(heroes => this.heroes = heroes); } add(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.create(name) .then(hero => { this.heroes.push(hero); this.selectedHero = null; }); } delete(hero: Hero): void { this.heroService .delete(hero.id) .then(() => { this.heroes = this.heroes.filter(h => h !== hero); if (this.selectedHero === hero) { this.selectedHero = null; } }); } ngOnInit(): void { this.getHeroes(); } onSelect(hero: Hero): void { this.selectedHero = hero; } gotoDetail(): void { this.router.navigate(['/detail', this.selectedHero.id]); } } <h2>My Heroes</h2> <div> <label>Hero name:</label> <input #heroName /> <button (click)="add(heroName.value); heroName.value=''"> Add </button> </div> <ul class="heroes"> <li *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero === selectedHero"> <span class="badge">{{hero.id}}</span> <span>{{hero.name}}</span> <button class="delete" (click)="delete(hero); $event.stopPropagation()">x</button> </li> </ul> <div *ngIf="selectedHero"> <h2> {{selectedHero.name | uppercase}} is my hero </h2> <button (click)="gotoDetail()">View Details</button> </div> .selected { background-color: #CFD8DC !important; color: white; } .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; } .heroes li { cursor: pointer; position: relative; left: 0; 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 li.selected:hover { background-color: #BBD8DC !important; color: white; } .heroes .text { position: relative; top: -3px; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; background-color: #607D8B; line-height: 1em; position: relative; left: -1px; top: -4px; height: 1.8em; margin-right: .8em; border-radius: 4px 0 0 4px; } button { font-family: Arial; background-color: #eee; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; cursor: hand; } button:hover { background-color: #cfd8dc; } button.delete { float:right; margin-top: 2px; margin-right: .8em; background-color: gray !important; color:white; } import 'rxjs/add/operator/switchMap'; import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; import { Location } from '@angular/common'; import { Hero } from './hero'; import { HeroService } from './hero.service'; @Component({ moduleId: module.id, selector: 'my-hero-detail', templateUrl: 'hero-detail.component.html', styleUrls: [ 'hero-detail.component.css' ] }) export class HeroDetailComponent implements OnInit { hero: Hero; constructor( private heroService: HeroService, private route: ActivatedRoute, private location: Location ) {} ngOnInit(): void { this.route.params .switchMap((params: Params) => this.heroService.getHero(+params['id'])) .subscribe(hero => this.hero = hero); } save(): void { this.heroService.update(this.hero) .then(() => this.goBack()); } goBack(): void { this.location.back(); } } <div *ngIf="hero"> <h2>{{hero.name}} details!</h2> <div> <label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="hero.name" placeholder="name" /> </div> <button (click)="goBack()">Back</button> <button (click)="save()">Save</button> </div> import { Injectable } from '@angular/core'; import { Headers, Http } from '@angular/http'; import 'rxjs/add/operator/toPromise'; import { Hero } from './hero'; @Injectable() export class HeroService { private headers = new Headers({'Content-Type': 'application/json'}); private heroesUrl = 'app/heroes'; // URL to web api constructor(private http: Http) { } getHeroes(): Promise<Hero[]> { return this.http.get(this.heroesUrl) .toPromise() .then(response => response.json().data as Hero[]) .catch(this.handleError); } getHero(id: number): Promise<Hero> { return this.getHeroes() .then(heroes => heroes.find(hero => hero.id === id)); } delete(id: number): Promise<void> { const url = `${this.heroesUrl}/${id}`; return this.http.delete(url, {headers: this.headers}) .toPromise() .then(() => null) .catch(this.handleError); } create(name: string): Promise<Hero> { return this.http .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers}) .toPromise() .then(res => res.json().data) .catch(this.handleError); } update(hero: Hero): Promise<Hero> { const url = `${this.heroesUrl}/${hero.id}`; return this.http .put(url, JSON.stringify(hero), {headers: this.headers}) .toPromise() .then(() => hero) .catch(this.handleError); } private handleError(error: any): Promise<any> { console.error('An error occurred', error); // for demo purposes only return Promise.reject(error.message || error); } } 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}; } } import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs'; import { Hero } from './hero'; @Injectable() export class HeroSearchService { constructor(private http: Http) {} search(term: string): Observable<Hero[]> { return this.http .get(`app/heroes/?name=${term}`) .map((r: Response) => r.json().data as Hero[]); } } import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; import { HeroSearchService } from './hero-search.service'; import { Hero } from './hero'; @Component({ moduleId: module.id, selector: 'hero-search', templateUrl: 'hero-search.component.html', styleUrls: [ 'hero-search.component.css' ], providers: [HeroSearchService] }) export class HeroSearchComponent implements OnInit { heroes: Observable<Hero[]>; private searchTerms = new Subject<string>(); constructor( private heroSearchService: HeroSearchService, private router: Router) {} // Push a search term into the observable stream. search(term: string): void { this.searchTerms.next(term); } ngOnInit(): void { this.heroes = this.searchTerms .debounceTime(300) // wait for 300ms pause in events .distinctUntilChanged() // ignore if next search term is same as previous .switchMap(term => term // switch to new observable each time // return the http search observable ? this.heroSearchService.search(term) // or the observable of empty heroes if no search term : Observable.of<Hero[]>([])) .catch(error => { // TODO: real error handling console.log(error); return Observable.of<Hero[]>([]); }); } gotoDetail(hero: Hero): void { let link = ['/detail', hero.id]; this.router.navigate(link); } } <div id="search-component"> <h4>Hero Search</h4> <input #searchBox id="search-box" (keyup)="search(searchBox.value)" /> <div> <div *ngFor="let hero of heroes | async" (click)="gotoDetail(hero)" class="search-result" > {{hero.name}} </div> </div> </div> .search-result{ border-bottom: 1px solid gray; border-left: 1px solid gray; border-right: 1px solid gray; width:195px; height: 20px; padding: 5px; background-color: white; cursor: pointer; } #search-box{ width: 200px; height: 20px; } // Observable class extensions import 'rxjs/add/observable/of'; import 'rxjs/add/observable/throw'; // Observable operators import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/filter'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/switchMap';

Next Step

下一步

Return to the learning path where you can read about the concepts and practices you discovered in this tutorial.

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