In the previous page, you learned to navigate between the dashboard and the fixed heroes list,
editing a selected hero along the way.
That's the starting point for this page.
保持应用的转译与运行
Keep the app transpiling and running
在终端窗口输入如下命令:
Enter the following command in the terminal window:
This command runs the TypeScript compiler in "watch mode", recompiling automatically when the code changes.
The command simultaneously launches the app in a browser and refreshes the browser when the code changes.
在后续构建《英雄指南》过程中,应用能持续运行,而不用中断服务来编译或刷新浏览器。
You can keep building the Tour of Heroes without pausing to recompile or refresh the browser.
The HttpModule is not a core Angular module.
HttpModule is Angular's optional approach to web access. It exists as a separate add-on module called @angular/http
and is shipped in a separate script file as part of the Angular npm package.
The app will depend on the Angular http service, which itself depends on other supporting services.
The HttpModule from the @angular/http library holds providers for a complete set of HTTP services.
Until you have a web server that can handle requests for hero data,
the HTTP client will fetch and save data from
a mock service, the in-memory web API.
修改src/app/app.module.ts,让它使用这个模拟服务:
Update src/app/app.module.ts with this version, which uses the mock service:
导入InMemoryWebApiModule并将其加入到模块的imports数组。
InMemoryWebApiModule将Http客户端默认的后端服务 —
这是一个辅助服务,负责与远程服务器对话 —
替换成了内存 Web API服务:
Rather than require a real API server, this example simulates communication with the remote server by adding the
InMemoryWebApiModule
to the module imports, effectively replacing the Http client's XHR backend service with an in-memory alternative.
The forRoot() configuration method takes an InMemoryDataService class
that primes the in-memory database.
Add the file in-memory-data.service.ts in app with the following content:
The in-memory web API is only useful in the early stages of development and for demonstrations such as this Tour of Heroes.
Don't worry about the details of this backend substitution; you can
skip it when you have a real web API server.
The Angular http.get returns an RxJS Observable.
Observables are a powerful way to manage asynchronous data flows.
You'll read about Observables later in this page.
There are many operators like toPromise that extend Observable with useful capabilities.
To use those capabilities, you have to add the operators themselves.
That's as easy as importing them from the RxJS library like this:
The response JSON has a single data property, which
holds the array of heroes that the caller wants.
So you grab that array and return it as the resolved Promise value.
仔细看看这个由服务器返回的数据的形态。
这个内存 Web API 的范例中所做的是返回一个带有data属性的对象。
你的 API 也可以返回其它东西。请调整这些代码以匹配你的 Web API。
Note the shape of the data that the server returns.
This particular in-memory web API example returns an object with a data property.
Your API might return something else. Adjust the code to match your web API.
When the HeroDetailComponent asks the HeroService to fetch a hero,
the HeroService currently fetches all heroes and
filters for the one with the matching id.
That's fine for a simulation, but it's wasteful to ask a real server for all heroes when you only want one.
Most web APIs support a get-by-id request in the form api/hero/:id (such as api/hero/11).
修改 HeroService.getHero() 方法来发起一个 get-by-id 请求:
Update the HeroService.getHero() method to make a get-by-id request:
Although you made significant internal changes to getHeroes() and getHero(),
the public signatures didn't change.
You still return a Promise from both methods.
You won't have to update any of the components that call them.
现在,我们该支持创建和删除英雄了。
Now it's time to add the ability to create and delete heroes.
Try editing a hero's name in the hero detail view.
As you type, the hero name is updated in the view heading.
But if you click the Back button, the changes are lost.
Updates weren't lost before. What changed?
When the app used a list of mock heroes, updates were applied directly to the
hero objects within the single, app-wide, shared list. Now that you're fetching data
from a server, if you want changes to persist, you must write them back to
the server.
我们通过一个编码在 URL 中的英雄 id 来告诉服务器应该更新哪个英雄。put 的 body 是该英雄的 JSON 字符串,它是通过调用JSON.stringify得到的。
并且在请求头中标记出的 body 的内容类型(application/json)。
To identify which hero the server should update, the hero id is encoded in
the URL. The put() body is the JSON string encoding of the hero, obtained by
calling JSON.stringify. The body content type
(application/json) is identified in the request header.
刷新浏览器试一下,对英雄名字的修改确实已经被持久化了。
Refresh the browser, change a hero name, save your change,
and click the browser Back button. Changes should now persist.
添加英雄
Add the ability to add heroes
要添加一个新的英雄,我们得先知道英雄的名字。我们使用一个 input 元素和一个添加按钮来实现。
To add a hero, the app needs the hero's name. You can use an input
element paired with an add button.
把下列代码插入 heroes 组件的 HTML 中,放在标题的下面:
Insert the following into the heroes component HTML, just after
the heading:
In addition to calling the component's delete() method, the delete button's
click handler code stops the propagation of the click event—you
don't want the <li> click handler to be triggered because doing so would
select the hero that the user will delete.
delete()处理器的逻辑略复杂:
The logic of the delete() handler is a bit trickier:
Of course you 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.
我们希望删除按钮被放在英雄条目的最右边。
于是 CSS 变成了这样:
To place the delete button at the far right of the hero entry,
add this CSS:
The HeroService converts that Observable into a Promise and returns the promise to the caller.
This section shows you how, when, and why to return the Observable directly.
背景
Background
一个可观察对象是一个事件流,我们可以用数组型操作符来处理它。
An Observable is a stream of events that you can process with array-like operators.
Angular core has basic support for observables.
Developers augment that support with operators and extensions from the
RxJS library.
You'll see how shortly.
Recall that the HeroService chained the toPromise operator to the Observable result of http.get().
That operator converted the Observable into a Promise and you passed that promise back to the caller.
Converting to a Promise is often a good choice. You typically ask http.get() to fetch a single chunk of data.
When you receive the data, you're done.
The calling component can easily consume a single result in the form of a Promise.
But requests aren't always done only once.
You may start one request,
cancel it, and make a different request before the server has responded to the first request.
You're going to add a hero search feature to the Tour of Heroes.
As the user types a name into a search box, you'll make repeated HTTP requests for heroes filtered by that name.
我们先创建HeroSearchService服务,它会把搜索请求发送到我们服务器上的 Web API。
Start by creating HeroSearchService that sends search queries to the server's web API.
More importantly, you no longer call toPromise().
Instead you return the Observable from the the http.get(),
after chaining it to another RxJS operator, map(),
to extract heroes from the response data.
But as you'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 you route it through the async pipe (AsyncPipe).
The async pipe subscribes to the Observable and produces the array of heroes to *ngFor.
该创建HeroSearchComponent类及其元数据了。
Create the HeroSearchComponent class and metadata.
.debounceTime(300)// wait 300ms after each keystroke before considering the term
.distinctUntilChanged()// ignore if next search term is same as previous
.switchMap(term => term // switch to new observable each time the term changes
// return the http search observable
?this.heroSearchService.search(term)
// or the observable of empty heroes if there was no search term
:Observable.of<Hero[]>([]))
.catch(error =>{
// TODO: add real error handling
console.log(error);
returnObservable.of<Hero[]>([]);
});
}
gotoDetail(hero:Hero):void{
let link =['/detail', hero.id];
this.router.navigate(link);
}
}
搜索词
Search terms
仔细看下这个searchTerms:
Focus on the searchTerms:
private searchTerms =newSubject<string>();
// Push a search term into the observable stream.
search(term:string):void{
this.searchTerms.next(term);
}
Subject(主题)是一个可观察的事件流中的生产者。
searchTerms生成一个产生字符串的Observable,用作按名称搜索时的过滤条件。Each call to search() puts a new string into this subject's observable stream by calling next().
A Subject is a producer of an observable event stream;
searchTerms produces an Observable of strings, the filter criteria for the name search.
Passing every user keystroke directly to the HeroSearchService would create an excessive amount of HTTP requests,
taxing server resources and burning through the cellular network data plan.
Instead, you can chain Observable operators that reduce the request flow to the string Observable.
You'll make fewer calls to the HeroSearchService and still get timely results. Here's how:
debounceTime(300) waits until the flow of new string events pauses for 300 milliseconds
before passing along the latest string. You'll never make requests more frequently than 300ms.
switchMap() calls the search service for each search term that makes it through debounce and distinctUntilChanged.
It cancels and discards previous search observables, returning only the latest search service observable.
With the switchMap operator
(formerly known as flatMapLatest),
every qualifying key event can trigger an http() method call.
Even with a 300ms pause between requests, you could have multiple HTTP requests in flight
and they may not return in the order sent.
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.
如果搜索框为空,我们还可以短路掉这次http()方法调用,并且直接返回一个包含空数组的可观察对象。
If the search text is empty, the http() method call is also short circuited
and an observable containing an empty array is returned.
Note that until the service supports that feature,canceling the HeroSearchService Observable
doesn't actually abort a pending HTTP request.
For now , unwanted resultsare discarded.
catch intercepts a failed observable.
The simple example prints the error to the console; a real life app would do better.
Then to clear the search result, you return an observable containing an empty array .
When you need more RxJS features, extend Observable by importing the libraries in which they are defined.
Here are all the RxJS imports that this component needs:
src/app/hero-search.component.ts (rxjs imports)
import{Observable}from'rxjs/Observable';import{Subject}from'rxjs/Subject';// Observable class extensionsimport'rxjs/add/observable/of';// Observable operatorsimport'rxjs/add/operator/catch';import'rxjs/add/operator/debounceTime';import'rxjs/add/operator/distinctUntilChanged';
You don't need the operator symbols themselves.
In each case, the mere act of importing the library
loads and executes the library's script file which, in turn, adds the operator to the Observable class.
为仪表盘添加搜索组件
Add the search component to the dashboard
将表示“英雄搜索”组件的 HTML 元素添加到DashboardComponent模版的最后面。
Add the hero search HTML element to the bottom of the DashboardComponent template.
src/app/dashboard.component.html
<h3>Top Heroes</h3><divclass="grid grid-pad"><a *ngFor="let hero of heroes" [routerLink]="['/detail', hero.id]"class="col-1-4"><divclass="module hero"><h4>{{hero.name}}</h4></div></a></div><hero-search></hero-search>
Finally, import HeroSearchComponent from
hero-search.component.ts
and add it to the declarations array.
src/app/app.module.ts (search)
declarations:[
AppComponent,
DashboardComponent,
HeroDetailComponent,
HeroesComponent,
HeroSearchComponent
],
再次运行该应用,跳转到仪表盘,并在英雄下方的搜索框里输入一些文本。
运行效果如下:
Run the app again. In the Dashboard, enter some text in the search box.
If you enter characters that match any existing hero names, you'll see something like this.