1021 lines
45 KiB
Plaintext
Raw Normal View History

- var _example = 'toh-6';
block includes
include ../_util-fns
- var _Http = 'Http'; // Angular `Http` library name.
- var _Angular_Http = 'Angular <code>Http</code>'
- var _Angular_http_library = 'Angular HTTP library'
- var _HttpModule = 'HttpModule'
- var _JSON_stringify = 'JSON.stringify'
:marked
# Getting and Saving Data with HTTP
2016-06-16 21:51:57 +08:00
# 通过HTTP获取与保存数据
2016-05-23 16:06:33 -04:00
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.
2016-06-16 21:51:57 +08:00
2016-06-30 17:08:34 +01:00
客户对我们的进展很满意!
现在,他们想要从服务器获取英雄数据,然后让用户添加、编辑和删除英雄,并且把这些修改结果保存回服务器。
2016-05-23 16:06:33 -04:00
In this chapter we teach our application to make the corresponding HTTP calls to a remote server's web API.
2016-06-16 21:51:57 +08:00
在这一章中我们要让应用程序学会通过HTTP调用来访问远程服务器上相应的Web API。
Run the <live-example></live-example> for this part.
2016-05-23 16:06:33 -04:00
2016-06-17 23:13:23 +08:00
p 运行这部分的#[+liveExampleLink2('在线例子', 'toh-6')]。
.l-main-section
:marked
## Where We Left Off
2016-06-17 23:13:23 +08:00
## 我们在哪儿
In the [previous chapter](toh-pt5.html), 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.
2016-06-17 23:13:23 +08:00
在[前一章](toh-pt5.html)中,我们学会了在仪表盘和固定的英雄列表之间导航,并编辑选定的英雄。这也就是本章的起点。
block start-server-and-watch
:marked
### 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:
打开terminal/console窗口输入下列命令来启动TypeScript编译器它会启动开发服务器并监视文件变更
code-example(language="bash").
npm start
:marked
The application runs and updates automatically as we continue to build the Tour of Heroes.
2016-06-17 23:13:23 +08:00
当我们继续构建《英雄指南》时,应用会运行并自动更新。
.l-main-section#http-providers
h1 Providing HTTP Services
2016-05-23 16:06:33 -04:00
h1 准备HTTP服务
2016-05-23 16:06:33 -04:00
block http-library
:marked
`Http` 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.
`Http`***并不是***Angular的核心模块。
2016-06-30 17:08:34 +01:00
它是Angular用来进行Web访问的一种可选方式并通过Angular包中一个名叫`@angular/http`的独立附属模块发布了出来。
2016-05-23 16:06:33 -04:00
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`中导入它做好了准备。
:marked
### Register (provide) *HTTP* services
2016-06-17 23:13:23 +08:00
### 注册(提供)*http*服务
2016-05-23 16:06:33 -04:00
block http-providers
:marked
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 the complete set of `http` services.
我们的应用将会依赖于Angular的`http`服务,它本身又依赖于其它支持类服务。
来自`@angular/http`库中的`HttpModule`保存着这些`http`相关服务提供商的全集。
2016-05-23 16:06:33 -04:00
:marked
We should be able to access `http` services from anywhere in the application.
So we register them in the `imports` array of `app.module.ts` where we
bootstrap the application and its root `AppComponent`.
2016-06-17 23:13:23 +08:00
我们要能从本应用的任何地方访问`http`服务,所以,就要在`app.module.ts`中的`imports`数组中注册它们。
这里同时也是我们引导应用及其根组件`AppComponent`的地方。
2016-05-23 16:06:33 -04:00
2016-08-24 14:11:32 +01:00
+makeExample('app/app.module.ts', 'v1','app/app.module.ts (v1)')
2016-06-17 23:13:23 +08:00
:marked
Notice that we supply `!{_HttpModule}` as part of the *imports* !{_array} in root NgModule `AppModule`.
2016-06-17 23:13:23 +08:00
注意,现在`!{_HttpModule}`已经是根模块`AppModule`的`imports`数组的一部分了。
.l-main-section
:marked
2016-08-24 10:23:22 +01:00
## Simulating the Web API
2016-06-17 23:13:23 +08:00
2016-08-24 10:23:22 +01:00
## 模拟Web API
We recommend registering application-wide services in the root
`!{_AppModuleVsAppComp}` *providers*. <span if-docs="dart">Here we're
registering in `main` for a special reason.</span>
2016-06-17 23:13:23 +08:00
我们建议在根模块`!{_AppModuleVsAppComp}`的`providers`数组中注册全应用级的服务。
<span if-docs="dart">Here we're registering in `main` for a special reason.</span>
2016-05-23 16:06:33 -04:00
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*.
2016-06-17 23:13:23 +08:00
我们的应用正处于开发的早期阶段,并且离进入产品阶段还很远。
我们甚至都还没有一个用来处理英雄相关请求的Web服务器在此之前*我们将不得不伪造一个*。
2016-05-23 16:06:33 -04:00
We're going to *trick* the HTTP client into fetching and saving data from
a mock service, the *in-memory web API*.
<span if-docs="dart"> 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`.</span>
2016-06-17 23:13:23 +08:00
我们要*耍点小花招*让http客户端从一个Mock服务*内存(in-memory)Web API*)中获取和保存数据。
<span if-docs="dart"> 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`.</span>
2016-05-23 16:06:33 -04:00
Here is a version of <span ngio-ex>!{_appModuleTsVsMainTs}</span> that performs this trick
2016-06-17 23:13:23 +08:00
这个版本的<span ngio-ex>!{_appModuleTsVsMainTs}</span>就是用来实现这个小花招的:
2016-06-17 23:13:23 +08:00
+makeExcerpt(_appModuleTsVsMainTs, 'v2')
block backend
:marked
We're importing the `InMemoryWebApiModule` and adding it to the module `imports`.
The `InMemoryWebApiModule` replaces the default `Http` client backend &mdash;
the supporting service that talks to the remote server &mdash;
with an _in-memory web API alternative service_.
2016-06-17 23:13:23 +08:00
导入`InMemoryWebApiModule`并将其加入到模块的`imports`数组。
将`InMemoryWebApiModule`将默认的负责与远程服务器交流的辅助`Http`服务替换成了*内存Web API*服务:
+makeExcerpt(_appModuleTsVsMainTs, 'in-mem-web-api', '')
:marked
The `forRoot` configuration method takes an `InMemoryDataService` class
that will prime the in-memory database as follows:
2016-05-23 16:06:33 -04:00
2016-08-24 14:11:32 +01:00
`forRoot`配置方法需要`InMemoryDataService`类实例,用来向内存数据库添加种子数据:
+makeExample('app/in-memory-data.service.ts', 'init')(format='.')
p This file replaces the #[code #[+adjExPath('mock-heroes.ts')]] which is now safe to delete.
p 该文件代替了#[code #[+adjExPath('mock-heroes.ts')]]文件,它现在可以安全的删除了。
2016-05-23 16:06:33 -04:00
block dont-be-distracted-by-backend-subst
.alert.is-helpful
:marked
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](../guide/server-communication.html#!#in-mem-web-api).
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客户端](../guide/server-communication.html#!#in-mem-web-api)一章。
记住,*内存Web API*只在开发的早期阶段有用,比如这个《英雄指南》。
如果你已经有了一个真实的Web API服务器请尽管跳过它。
.l-main-section
:marked
## Heroes and HTTP
2016-05-23 16:06:33 -04:00
2016-06-17 23:13:23 +08:00
## 英雄与Http
Look at our current `HeroService` implementation
2016-06-17 23:13:23 +08:00
来看看我们目前的`HeroService`的实现
+makeExcerpt('toh-4/ts/app/hero.service.ts (old getHeroes)', 'get-heroes')
2016-06-17 23:13:23 +08:00
:marked
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.
2016-05-23 16:06:33 -04:00
2016-06-17 23:13:23 +08:00
我们返回一个promise它用mock版的英雄列表进行解析。
在当时它可能看起来做得有点过分了不过那时候我们就预料到有这么这一天会通过一个http客户端来获取英雄数据而且我们知道那必然是一个异步操作。
That day has arrived! Let's convert `getHeroes()` to use HTTP:
2016-05-23 16:06:33 -04:00
2016-06-30 17:08:34 +01:00
这一天到来了!我们把`getHeroes()`换成用HTTP。
2016-06-17 23:13:23 +08:00
+makeExcerpt('app/hero.service.ts (new constructor and revised getHeroes)', 'getHeroes')
2016-05-23 16:06:33 -04:00
:marked
### HTTP !{_Promise}
2016-06-17 23:13:23 +08:00
### Http承诺(Promise)
We're still returning a !{_Promise} but we're creating it differently.
2016-06-17 23:13:23 +08:00
2016-06-30 17:08:34 +01:00
我们仍然返回一个承诺,但是用不同的方法来创建它。
block get-heroes-details
:marked
The Angular `http.get` returns an RxJS `Observable`.
*Observables* are a powerful way to manage asynchronous data flows.
We'll learn about [Observables](#observables) later in this chapter.
Angular的`http.get`返回一个RxJS的`Observable`对象。
*Observable(可观察对象)*是一个管理异步数据流的强力方式。
后面我们还会进一步学习[可观察对象](#observables)。
2016-05-23 16:06:33 -04:00
For *now* we get back on familiar ground by immediately by
converting that `Observable` to a `Promise` using the `toPromise` operator.
2016-06-17 23:13:23 +08:00
*现在*,我们先利用`toPromise`操作符把`Observable`直接转换成`Promise`对象,回到已经熟悉的地盘。
+makeExcerpt('app/hero.service.ts', 'to-promise', '')
:marked
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`只是一个骨架实现。
2016-06-17 23:13:23 +08:00
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库中导入它们就可以了就像这样
+makeExcerpt('app/hero.service.ts', 'rxjs', '')
2016-06-17 23:13:23 +08:00
:marked
### 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.
在*promise*的`then`回调中我们调用http的`Reponse`对象的`json`方法,以提取出其中的数据。
2016-06-17 23:13:23 +08:00
+makeExcerpt('app/hero.service.ts', 'to-data', '')
2016-06-17 23:13:23 +08:00
:marked
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.
2016-06-17 23:13:23 +08:00
这个由`json`方法返回的对象只有一个`data`属性。
这个`data`属性保存了*英雄*数组,这个数组才是调用者真正想要的。
所以我们取得这个数组,并且把它作为承诺的值进行解析。
.alert.is-important
:marked
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.
2016-05-23 16:06:33 -04:00
2016-06-17 23:13:23 +08:00
仔细看看这个由服务器返回的数据的形态。
这个*内存Web API*的范例中所做的是返回一个带有`data`属性的对象。
你的API也可以返回其它东西。
Adjust the code to match *your web API*.
2016-06-17 23:13:23 +08:00
请随意调整这些代码,以适应*你自己的Web API*。
:marked
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`.
2016-06-17 23:13:23 +08:00
调用者不关心这些实现机制,它仍然像以前那样取得一个包含*英雄数据*的承诺。
它不关心我们已经改成了从服务器获取英雄数据。
它也不了解把http回应转换成英雄数据时所作的这些复杂变换。
:marked
### Error Handling
2016-05-23 16:06:33 -04:00
2016-06-17 23:13:23 +08:00
### 错误处理
At the end of `getHeroes()` we `catch` server failures and pass them to an error handler:
2016-06-17 23:13:23 +08:00
在`getHeroes()`的最后,我们`catch`了服务器的失败信息,并把它们传给了错误处理器:
2016-06-17 23:13:23 +08:00
+makeExcerpt('app/hero.service.ts', 'catch', '')
2016-06-17 23:13:23 +08:00
:marked
2016-05-23 16:06:33 -04:00
This is a critical step!
We must anticipate HTTP failures as they happen frequently for reasons beyond our control.
2016-06-17 23:13:23 +08:00
这是一个关键的步骤!
我们必须预料到http请求会失败因为有太多我们无法控制的原因可能导致它们频繁出现各种错误。
+makeExcerpt('app/hero.service.ts', 'handleError', '')
- var rejected_promise = _docsFor == 'dart' ? 'propagated exception' : 'rejected promise';
:marked
In this demo service we log the error to the console; we should do better in real life.
2016-06-17 23:13:23 +08:00
在这个范例服务中,我们把错误记录到控制台中;在真实世界中,我们应该做得更好。
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.
2016-05-23 16:06:33 -04:00
2016-06-17 23:13:23 +08:00
我们还要通过一个被拒绝(rejected)的承诺(promise)来把该错误用一个用户友好的格式返回给调用者,以便调用者能把一个合适的错误信息显示给用户。
### !{_Promise}s are !{_Promise}s
2016-06-17 23:13:23 +08:00
### 承诺仍然是承诺
2016-05-23 16:06:33 -04:00
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()`.
2016-06-17 23:13:23 +08:00
虽然我们对`getHeroes()`做了一些重要的*内部*修改,该方法公开的函数签名却没有任何变化。
我们返回的仍然是一个承诺,不用被迫更新任何一个调用了`getHeroes()`的组件。
2016-05-23 16:06:33 -04:00
.l-main-section
:marked
## Add, Edit, Delete
2016-05-23 16:06:33 -04:00
2016-06-17 23:13:23 +08:00
## 添加、编辑、删除
Our stakeholders are incredibly pleased with the added flexibility from the API integration, but it doesn't stop there. Next we want to add the capability to add, edit and delete heroes.
2016-05-23 16:06:33 -04:00
2016-06-17 23:13:23 +08:00
我们的客户对这种富有弹性的API集成方式感到非常高兴但事情可不会到此为止。下一步我们还要增加对英雄的添加、编辑和删除操作。
We'll complete `HeroService` by creating `post`, `put` and `delete` methods to meet our new requirements.
2016-06-30 20:35:04 +01:00
我们将为`HeroService`添加`post`、`put`和`delete`的http调用来满足我们的需求。
2016-06-17 23:13:23 +08:00
:marked
### Post
2016-05-23 16:06:33 -04:00
2016-06-17 23:13:23 +08:00
### Post
2016-08-23 15:52:39 +01:00
We will be using `post` to add new heroes. Post requests require a little bit more setup than Get requests:
2016-08-23 15:52:39 +01:00
我们使用`post`来添加新的英雄。post请求比get请求稍多一点儿设置工作
2016-06-17 23:13:23 +08:00
+makeExcerpt('app/hero.service.ts', 'post')
:marked
For Post requests we create a header and set the content type to `application/json`. We'll call `!{_JSON_stringify}` before we post to convert the hero object to a string.
2016-05-23 16:06:33 -04:00
2016-06-17 23:13:23 +08:00
现在,我们创建了一个请求头,并把内容类型(content-type)设置为`application/json`。在我们post之前要先调用`JSON.stringify`来把这个`hero`对象转换成一个字符串。
### Put
### Put
2016-05-23 16:06:33 -04:00
Put will be used to update an individual hero. Its structure is very similar to Post requests. The only difference is that we have to change the URL slightly by appending the id of the hero we want to update.
2016-06-29 10:39:12 +08:00
Put用来编辑一个指定的英雄但是代码结构和POST请求很相似。唯一的不同是我们要修改URL为它附加上我们想要编辑的那位英雄的id。
+makeExcerpt('app/hero.service.ts', 'put')
:marked
### Delete
2016-06-17 23:13:23 +08:00
### Delete
Delete will be used to delete heroes and its format is like `put` except for the function name.
Delete用来删除英雄它的书写方式几乎和Put完全相同 —— 除了函数名。
2016-06-17 23:13:23 +08:00
+makeExcerpt('app/hero.service.ts', 'delete')
:marked
We add a `catch` to handle errors for all three methods.
2016-05-23 16:06:33 -04:00
2016-06-17 23:13:23 +08:00
我们添加了`catch`,用来处理这三种情况下的错误。
:marked
### Save
2016-06-17 23:13:23 +08:00
### Save
We combine the call to the private `post` and `put` methods in a single `save` method. This simplifies the public API and makes the integration with `HeroDetailComponent` easier. `HeroService` determines which method to call based on the state of the `hero` object. If the hero already has an id we know it's an edit. Otherwise we know it's an add.
2016-06-17 23:13:23 +08:00
我们把对私有方法`post`和`put`的调用组合进一个单独的`save`方法。这会简化公开的API以简化与`HeroDetailComponent`的集成。
`HeroService`会根据`hero`对象的状态来决定该调用哪个方法。如果这个英雄已经有了一个id我们就知道它应该是编辑操作否则就是添加。
+makeExcerpt('app/hero.service.ts', 'save')
2016-05-23 16:06:33 -04:00
:marked
After these additions our `HeroService` looks like this:
2016-06-17 23:13:23 +08:00
填加完所有这些之后,我们的`HeroService`看起来像这样:
+makeExample('app/hero.service.ts')
.l-main-section
:marked
## Updating Components
2016-05-23 16:06:33 -04:00
2016-06-17 23:13:23 +08:00
## 更新组件
2016-05-23 16:06:33 -04:00
Loading heroes using `Http` required no changes outside of `HeroService`, but we added a few new features as well.
In the following section we will update our components to use our new methods to add, edit and delete heroes.
2016-06-17 23:13:23 +08:00
使用`Http`加载英雄数据并不需要在`HeroService`外部做任何修改,但我们却顺利的添加了一些新特性。
在后面的小节中,我们将更新我们的组件来调用这些新方法,以添加、编辑和删除英雄。
block hero-detail-comp-extra-imports-and-vars
:marked
Before we can add those methods, we need to initialize some variables with their respective imports.
在这么做之前我们先得用它们各自的import语句来初始化一些变量。
2016-06-17 23:13:23 +08:00
+makeExcerpt('app/hero-detail.component.ts ()', 'variables-imports')
block hero-detail-comp-updates
:marked
### Add/Edit in the *HeroDetailComponent*
2016-06-30 20:35:04 +01:00
### 在*HeroDetailComponent*中添加和编辑英雄
We already have `HeroDetailComponent` for viewing details about a specific hero.
Add and Edit are natural extensions of the detail view, so we are able to reuse `HeroDetailComponent` with a few tweaks.
我们已经有了`HeroDetailComponent`,用以查看指定英雄的详情。
添加和编辑功能是对详情页的自然扩展,所以我们可以复用`HeroDetailComponent`,只要做少量修改就可以了。
2016-06-17 23:13:23 +08:00
The original component was created to render existing data, but to add new data we have to initialize the `hero` property to an empty `Hero` object.
该组件原本是用来渲染现存数据的,要想添加新的数据,我们就得把`hero`属性初始化在一个空的`Hero`对象。
2016-05-23 16:06:33 -04:00
+makeExcerpt('app/hero-detail.component.ts', 'ngOnInit')
2016-06-17 23:13:23 +08:00
:marked
In order to differentiate between add and edit we are adding a check to see if an id is passed in the URL. If the id is absent we bind `HeroDetailComponent` to an empty `Hero` object. In either case, any edits made through the UI will be bound back to the same `hero` property.
2016-08-06 20:04:41 +08:00
为了区分添加和编辑操作我们增加了一步检查看看URL中是否传入了id。如果没有id我们就把`HeroDetailComponent`绑定到一个空的`Hero`对象。
无论是哪种情况通过UI进行的任何编辑操作都会被绑定回同一个`hero`属性。
:marked
Add a save method to `HeroDetailComponent` and call the corresponding save method in `HeroesService`.
2016-05-23 16:06:33 -04:00
2016-06-30 20:35:04 +01:00
在`HeroDetailComponent`中添加一个`save`方法,用来从`HeroService`调用对应的`save`方法
+makeExcerpt('app/hero-detail.component.ts', 'save')
2016-06-17 23:13:23 +08:00
block hero-detail-comp-save-and-goback
:marked
The same save method is used for both add and edit since `HeroService` will know when to call `post` vs `put` based on the state of the `Hero` object.
无论添加还是编辑,所用的都是同一个`save`方法,这是因为`HeroService`会根据`Hero`对象的状态判断出什么时候该调用`post`什么时候该调用`put`。
After we save a hero, we redirect the browser back to the previous page using the `goBack()` method.
在我们保存了英雄之后,我们使用`goBack()`方法让浏览器回到前一个页面。
+makeExcerpt('app/hero-detail.component.ts', 'goBack')
2016-06-17 23:13:23 +08:00
:marked
Here we call `emit` to notify that we just added or modified a hero. `HeroesComponent` is listening for this notification and will automatically refresh the list of heroes to include our recent updates.
这里我们调用了`emit`来通知别人:我们刚刚添加或修改了一个英雄。`HeroesComponent`正在监听这个通知,并自动刷新英雄列表,以体现我们最近所做的修改。
.l-sub-section
:marked
The `emit` "handshake" between `HeroDetailComponent` and `HeroesComponent` is an example of component to component communication. This is a topic for another day, but we have detailed information in our <a href="/docs/ts/latest/cookbook/component-communication.html#!#child-to-parent">Component Interaction Cookbook</a>
`emit`在`HeroDetailComponent`和`HeroesComponent`之间的这种握手是组件间通讯的例子之一。
2016-06-30 20:35:04 +01:00
这不是今天的话题,在<a href="/docs/ts/latest/cookbook/component-communication.html#!#child-to-parent">组件间交互</a>一章中有相关详细信息。
2016-06-17 23:13:23 +08:00
:marked
Here is `HeroDetailComponent` with its new save button and the corresponding HTML.
2016-05-23 16:06:33 -04:00
2016-06-17 23:13:23 +08:00
这里是`HeroDetailComponent`及其新的“保存”按钮。
figure.image-display
img(src='/resources/images/devguide/toh/hero-details-save-button.png' alt="Hero Details With Save Button")
+makeExcerpt('app/hero-detail.component.html', 'save')
:marked
### Add/Delete in the *HeroesComponent*
2016-05-23 16:06:33 -04:00
### 在`HeroesComponent`中添加/删除
2016-06-17 23:13:23 +08:00
We'll be reporting propagated HTTP errors, let's start by adding the following
field to the `HeroesComponent` class:
我们将报告所出现的HTTP错误从往`HeroesComponent`类中添加下列字段开始:
+makeExcerpt('app/heroes.component.ts', 'error', '')
2016-06-17 23:13:23 +08:00
:marked
The user can *add* a new hero by clicking a button and entering a name.
2016-05-23 16:06:33 -04:00
用户可以通过点击一个按钮并输入英雄的名字来*添加*一个新的英雄。
2016-06-17 23:13:23 +08:00
block add-new-hero-via-detail-comp
:marked
When the user clicks the *Add New Hero* button, we display the `HeroDetailComponent`.
We aren't navigating to the component so it won't receive a hero `id`;
as we noted above, that is the component's cue to create and present an empty hero.
当用户点击*Add New Hero*按钮时,我们显示`HeroDetailComponent`。我们不会导航到那个组件,所以它也不会接收到英雄的`id`
正如我们以前提过的,组件会据此创建并展示一个空白的英雄。
2016-06-17 23:13:23 +08:00
- var _below = _docsFor == 'dart' ? 'before' : 'below';
:marked
Add the following to the heroes component HTML, just !{_below} the hero list (`<ul class="heroes">...</ul>`).
2016-06-17 23:13:23 +08:00
往`heroes.component.html`中,英雄列表(`*ngFor`)的紧下方添加下列HTML。
+makeExcerpt('app/heroes.component.html', 'add-and-error')
:marked
The first line will display an error message if there is any. The remaining HTML is for adding heroes.
如果有错误信息它将显示在第一行。剩下的HTML是用来添加英雄的。
2016-05-23 16:06:33 -04:00
The user can *delete* an existing hero by clicking a delete button next to the hero's name.
Add the following to the heroes component HTML right after the hero name in the repeated `<li>` tag:
2016-06-17 23:13:23 +08:00
用户可以通过点击英雄名后面的删除按钮来*删除*一个现存的英雄。
往`heroes.component.html`中往`<li>`标签中名字的紧后面添加下列HTML代码
+makeExcerpt('app/heroes.component.html', 'delete')
:marked
Add the following to the bottom of the `HeroesComponent` CSS file:
把下列代码添加到`HeroesComponent`的CSS文件底部
+makeExcerpt('app/heroes.component.css', 'additions')
:marked
Now let's fix-up the `HeroesComponent` to support the *add* and *delete* actions used in the template.
Let's start with *add*.
2016-05-23 16:06:33 -04:00
2016-06-30 20:35:04 +01:00
现在,让我们修复`HeroesComponent`,让它支持模板中的*添加*和*删除*动作。以*添加*动作开始。
Implement the click handler for the *Add New Hero* button.
然后我们实现*Add New Hero*按钮的点击事件处理器。
+makeExcerpt('app/heroes.component.ts', 'addHero')
2016-06-17 23:13:23 +08:00
block heroes-comp-add
:marked
The `HeroDetailComponent` does most of the work. All we do is toggle an `*ngIf` flag that
swaps it into the DOM when we add a hero and removes it from the DOM when the user is done.
`HeroDetailComponent`做了大部分工作。我们所要做的就是切换`*ngIf`的标志量当用户添加英雄时把它放进DOM中当用户添加完毕时把它从DOM中移除。
2016-06-17 23:13:23 +08:00
:marked
The *delete* logic is a bit trickier.
2016-06-17 23:13:23 +08:00
*删除*逻辑略有点棘手。
+makeExcerpt('app/heroes.component.ts', 'deleteHero')
:marked
2016-05-23 16:06:33 -04:00
Of course we delegate the persistence of hero deletion to the `HeroService`.
But the component is still responsible for updating the display.
So the *delete* method removes the deleted hero from the list.
2016-06-30 20:35:04 +01:00
当然,我们把删除英雄并让其持久有效的工作交给`HeroesService`。
但是组件仍然需要负责更新显示。
所以*delete*方法从英雄列表移除被删除的英雄。
block review
:marked
### Let's see it
### 我们来看看
Here are the fruits of labor in action:
下面是这些劳动成果的操作演示:
2016-06-17 23:13:23 +08:00
figure.image-display
img(src='/resources/images/devguide/toh/toh-http.anim.gif' alt="Heroes List Editing w/ HTTP")
2016-08-23 15:52:39 +01:00
:marked
## !{_Observable}s
## 可观察对象Observable
block observables-section-intro
:marked
Each `Http` 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](http://reactivex.io/rxjs/) library.
We'll see how shortly.
Angular内核中提供了对可观察对象的基本支持。而我们这些开发人员可以自己从[RxJS可观察对象](http://reactivex.io/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` 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`获取单块数据。只要接收到数据,就算完成。
使用承诺这种形式的结果是让调用方更容易写并且承诺已经在JavaScript程序员中被广泛接受了。
:marked
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 *!{_Promise}s*.
It's easy with *!{_Observable}s* 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.
我们要为《英雄指南》添加一个*英雄搜索*特性。
2016-08-06 20:04:41 +08:00
当用户在搜索框中输入一个名字时我们将不断发起HTTP请求以获得按名字过滤的英雄。
We start by creating `HeroSearchService` that sends search queries to our server's web api.
我们先创建`HeroSearchService`服务它会把搜索请求发送到我们服务器上的Web API。
+makeExample('app/hero-search.service.ts')
:marked
The `!{_priv}http.get()` call in `HeroSearchService` is similar to the one
in the `HeroService`, although the URL now has a query string.
<span if-docs="ts">Another notable difference: we no longer call `toPromise`,
we simply return the *observable* instead.</span>
`HeroSearchService`中的`http.get()`调用和`HeroService`中的很相似,只是这次带了查询字符串。
<span if-docs="ts">显著的不同是:我们不再调用`toPromise`,而是直接返回*可观察对象*。</span>
### HeroSearchComponent
### HeroSearchComponent
Let's create a new `HeroSearchComponent` that calls this new `HeroSearchService`.
我们再创建一个新的`HeroSearchComponent`来调用这个新的`HeroSearchService`。
The component template is simple &mdash; just a text box and a list of matching search results.
组件模板很简单,就是一个输入框和一个相匹配的搜索结果列表。
+makeExample('app/hero-search.component.html')
:marked
We'll also want to add styles for the new component.
我们还要往这个新组件中添加样式。
+makeExample('app/hero-search.component.css')
:marked
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 !{_array}s, 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`对象,而不再只是英雄数组。
2016-08-06 20:04:41 +08:00
`*ngFor`不能用可观察对象做任何事,除非我们在它后面跟一个`async` pipe (`AsyncPipe`)。
这个`async`管道会订阅到这个可观察对象,并且为`*ngFor`生成一个英雄数组。
Time to create the `HeroSearchComponent` class and metadata.
该创建`HeroSearchComponent`类及其元数据了。
+makeExample('app/hero-search.component.ts')
:marked
#### Search terms
#### 搜索词
Let's focus on the `!{_priv}searchTerms`:
仔细看下这个`searchTerms`
+makeExcerpt('app/hero-search.component.ts', 'searchTerms', '')
block search-criteria-intro
:marked
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`来把新的字符串放进该主体的_可观察_流中。
:marked
<a id="ngoninit"></a>
#### Initialize the _**heroes**_ property (_**ngOnInit**_)
#### 初始化_**heroes**_属性(_**ngOnInit**_)
<span if-docs="ts">A `Subject` is also an `Observable`.</span>
We're going to turn the stream
of search terms into a stream of `Hero` !{_array}s and assign the result to the `heroes` property.
<span if-docs="ts">`Subject`也是一个`Observable`对象。</span>
我们要把字符串数组的流转换成`Hero`数组的流,并把结果赋值给`heroes`属性。
+makeExcerpt('app/hero-search.component.ts', 'search', '')
:marked
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请求风暴。
这可不好玩。我们不希望占用服务器资源,也不想耗尽网络带宽。
block observable-transformers
:marked
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`发起更少的调用,并且仍然获得足够及时的响应。做法如下:
* `debounceTime(300)` waits until the flow of new string events pauses for 300 milliseconds
before passing along the latest string. We'll never make requests more frequently than 300ms.
* 在传出最终字符串之前,`debounceTime(300)`将会等待直到新增字符串的事件暂停了300毫秒。我们实际发起请求的间隔永远不会小于300ms。
* `distinctUntilChanged` ensures that we only send a request if the filter text changed.
There's no point in repeating a request for the same search term.
2016-07-25 12:42:36 +08:00
* `distinctUntilChanged`确保只在过滤条件变化时才发送请求,这样就不会重复请求同一个搜索词了。
* `switchMap` calls our search service for each search term that makes it through the `debounce` and `distinctUntilChanged` gauntlet.
It cancels and discards previous search observables, returning only the latest search service observable.
2016-07-25 12:42:36 +08:00
* `switchMap`会为每个从`debounce`和`distinctUntilChanged`中通关的搜索词调用搜索服务。它会丢弃以前的搜索Observable只保留最近的。
.l-sub-section
:marked
The [switchMap operator](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md)
(formerly known as "flatMapLatest") is very clever.
2016-07-25 12:42:36 +08:00
[switchMap操作符](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md)
(以前叫"flatMapLatest")是非常智能的。
Every qualifying key event can trigger an http 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.
2016-07-25 12:42:36 +08:00
每次符合条件的按键事件都会触发一次http调用。即使在发送每个请求前都有300毫秒的延迟我们仍然可能同时拥有多个在途的HTTP请求并且它们返回的顺序未必就是发送时的顺序。
`switchMap` preserves the original request order while returning
only the observable from the most recent http call.
Results from prior calls are canceled and discarded.
2016-07-25 12:42:36 +08:00
`switchMap`保留了原始的请求顺序并且只返回最近一次http调用返回的可观察对象。
这是因为以前的调用都被取消或丢弃了。
We also short-circuit the http call and return an observable containing an empty array
if the search text is empty.
2016-07-25 12:42:36 +08:00
如果搜索框为空我们还可以短路掉这次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.
2016-07-25 12:42:36 +08:00
注意_取消_`HeroSearchService`的可观察对象并不会实际中止abort一个未完成的http请求除非有一天我们支持了这个特性这个问题我们以后再讨论。
2016-07-26 08:16:57 +08:00
目前我们的做法只是丢弃不希望的结果。
2016-07-25 12:42:36 +08:00
:marked
* `catch` intercepts a failed observable.
Our simple example prints the error to the console; a real life application should do better.
Then we return an observable containing an empty array to clear the search result.
2016-07-25 12:42:36 +08:00
* `catch`拦截失败的Observable。这个简单的例子中只是把错误信息打印到控制台但实际的应用需要做更多事然后返回一个包含空数组的可观察对象以清空搜索结果。
### Import RxJS operators
2016-07-25 12:42:36 +08:00
### 导入RxJS操作符
The RxJS operators are not available in Angular's base `Observable` implementation.
We have to extend `Observable` by *importing* them.
2016-07-25 12:42:36 +08:00
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.
2016-07-25 12:42:36 +08:00
通过在本文件的顶部写上适当的`import`语句,我们可以为`Observable`扩展出这里用到的那些操作符。
.l-sub-section
:marked
Many authorities say we should do just that.
2016-07-25 12:42:36 +08:00
有很多权威人士建议我们这样做。
:marked
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.
2016-07-25 12:42:36 +08:00
在这个例子中,我们使用一些不同的方法。
我们把整个应用中要用的那些RxJS `Observable`扩展组合在一起放在一个单独的RxJS导入文件中。
+makeExample('app/rxjs-extensions.ts')(format='.')
:marked
We load them all at once by importing `rxjs-extensions` in `AppComponent`.
2016-07-25 12:42:36 +08:00
我们在`AppComponent`中导入`rxjs-extensions`就可以一次性加载它们。
+makeExcerpt('app/app.component.ts', 'rxjs-extensions')(format='.')
:marked
### Add the search component to the dashboard
### 为仪表盘添加搜索组件
We add the hero search HTML element to the bottom of the `DashboardComponent` template.
将表示“英雄搜索”组件的HTML元素添加到`DashboardComponent`模版的最后面。
+makeExample('app/dashboard.component.html')(format='.')
- var _declarations = _docsFor == 'dart' ? 'directives' : 'declarations'
- var declFile = _docsFor == 'dart' ? 'app/dashboard.component.ts' : 'app/app.module.ts'
:marked
And finally, we import the `HeroSearchComponent` from `'./hero-search.component.ts'`
and add it to the `!{_declarations}` !{_array}:
最后,从`'./hero-search.component.ts'` 导入`HeroSearchComponent`并将其添加到`declarations`数组中。
+makeExcerpt(declFile, 'search')
:marked
Run the app again, go to the *Dashboard*, and enter some text in the search box below the hero tiles.
At some point it might look like this.
再次运行该应用,跳转到*Dashboard*,并在英雄下方的搜索框里输入一些文本。
看起来就像这样:
figure.image-display
img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component")
.l-main-section
:marked
## Application structure and code
2016-06-17 23:13:23 +08:00
## 应用的结构与代码
Review the sample source code in the <live-example></live-example> for this chapter.
Verify that we have the following structure:
在<live-example></live-example>中回顾本章的范例代码。
验证我们是否得到了如下结构:
block filetree
.filetree
.file angular2-tour-of-heroes
.children
.file app
.children
.file app.component.ts
.file app.component.css
.file app.module.ts
.file app.routing.ts
.file dashboard.component.css
.file dashboard.component.html
.file dashboard.component.ts
.file hero.ts
.file hero-detail.component.css
.file hero-detail.component.html
.file hero-detail.component.ts
.file hero-search.component.html (new)
.file hero-search.component.css (new)
.file hero-search.component.ts (new)
.file hero-search.service.ts (new)
.file rxjs-extensions.ts
.file hero.service.ts
.file heroes.component.css
.file heroes.component.html
.file heroes.component.ts
.file main.ts
.file in-memory-data.service.ts (new)
.file node_modules ...
.file typings ...
.file index.html
.file package.json
.file styles.css
.file systemjs.config.js
.file tsconfig.json
.file typings.json
2016-05-23 16:06:33 -04:00
.l-main-section
:marked
## Home Stretch
2016-07-26 08:16:57 +08:00
## 最后冲刺
2016-05-23 16:06:33 -04:00
We are at the end of our journey for now, but we have accomplished a lot.
2016-06-17 23:13:23 +08:00
旅程即将结束,不过我们已经收获颇丰。
2016-06-17 23:13:23 +08:00
- We added the necessary dependencies to use HTTP in our application.
2016-06-17 23:13:23 +08:00
- 我们添加了在应用程序中使用HTTP的必备依赖。
2016-06-17 23:13:23 +08:00
- We refactored `HeroService` to load heroes from a web API.
2016-06-17 23:13:23 +08:00
- 我们重构了`HeroService`以通过web API来加载英雄数据。
2016-06-17 23:13:23 +08:00
- We extended `HeroService` to support post, put and delete methods.
2016-06-17 23:13:23 +08:00
- 我们扩展了`HeroService`来支持post、put和delete方法。
2016-06-17 23:13:23 +08:00
- We updated our components to allow adding, editing and deleting of heroes.
2016-06-17 23:13:23 +08:00
- 我们更新了组件,以允许用户添加、编辑和删除英雄。
2016-06-17 23:13:23 +08:00
- We configured an in-memory web API.
2016-05-23 16:06:33 -04:00
- 我们配置了一个内存Web API。
- We learned how to use !{_Observable}s.
- 我们学会了如何使用“可观察对象”。
Here are the files we added or changed in this chapter.
下面是我们添加或修改之后的文件汇总。
block file-summary
+makeTabs(
`toh-6/ts/app/app.component.ts,
toh-6/ts/app/app.module.ts,
toh-6/ts/app/heroes.component.ts,
toh-6/ts/app/heroes.component.html,
toh-6/ts/app/heroes.component.css,
toh-6/ts/app/hero-detail.component.ts,
toh-6/ts/app/hero-detail.component.html,
toh-6/ts/app/hero.service.ts,
toh-6/ts/app/in-memory-data.service.ts`,
',,,,,,,,',
`app.comp...ts,
app.mod...ts,
heroes.comp...ts,
heroes.comp...html,
heroes.comp...css,
hero-detail.comp...ts,
hero-detail.comp...html,
hero.service.ts,
in-memory-data.service.ts`
)
+makeTabs(
`toh-6/ts/app/hero-search.service.ts,
toh-6/ts/app/hero-search.component.ts,
toh-6/ts/app/hero-search.component.html,
toh-6/ts/app/hero-search.component.css,
toh-6/ts/app/rxjs-extensions.ts`,
null,
`hero-search.service.ts,
hero-search.component.ts,
hero-search.component.html,
hero-search.component.css,
rxjs-extensions.ts`
)