angular-cn/public/docs/ts/latest/guide/server-communication.jade
2016-05-25 11:20:05 +08:00

947 lines
52 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 _Angular_http_libraryCn = 'Angular HTTP库'
:marked
[HTTP](https://tools.ietf.org/html/rfc2616) is the primary protocol for browser/server communication.
[HTTP](https://tools.ietf.org/html/rfc2616)是浏览器和服务器之间通讯的主要协议。
.l-sub-section
:marked
The [`WebSocket`](https://tools.ietf.org/html/rfc6455) protocol is another important communication technology;
we won't cover it in this chapter.
[`WebSocket`](https://tools.ietf.org/html/rfc6455)协议是另一种重要的通讯技术,但我们本章不会涉及它。
:marked
Modern browsers support two HTTP-based APIs:
[XMLHttpRequest (XHR)](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) and
[JSONP](https://en.wikipedia.org/wiki/JSONP). A few browsers also support
[Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
现代浏览器支持两种基于HTTP的API
[XMLHttpRequest (XHR)](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)和
[JSONP](https://en.wikipedia.org/wiki/JSONP)。少数浏览器还支持
[Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)。
The !{_Angular_http_library} simplifies application programming of the **XHR** and **JSONP** APIs
as we'll learn in this chapter covering:
!{_Angular_http_libraryCn}简化了**XHR**和**JSONP**的编程,这就是本章所要讲的。
ul
li #[a(href="#http-client") HTTP client sample overview]
li #[a(href="#http-client") HTTP客户端范例概览]
li #[a(href="#fetch-data") Fetch data with http.get]
li #[a(href="#fetch-data") 通过http.get获取数据]
+ifDocsFor('ts')
li #[a(href="#rxjs") RxJS Observable of HTTP Responses]
li #[a(href="#rxjs") HTTP响应中的RxJS 可观察对象(Observable)]
li #[a(href="#enable-rxjs-operators") Enabling RxJS Operators]
li #[a(href="#enable-rxjs-operators") 启用RxJS操作(Operator)函数]
li #[a(href="#extract-data") Extract JSON data]
li #[a(href="#extract-data") 提取JSON数据]
li #[a(href="#error-handling") Error handling]
li #[a(href="#error-handling") 错误处理]
li #[a(href="#update") Send data to the server]
li #[a(href="#update") 把数据发送到服务器]
+ifDocsFor('ts')
li #[a(href="#promises") Promises instead of observables]
li #[a(href="#promises") 把承诺(Promise)换成可观察对象(Observable)]
li #[a(href="#cors") Cross-origin requests: Wikipedia example]
li #[a(href="#cors") 跨域请求以Wikipedia为例]
+ifDocsFor('ts')
ul
li #[a(href="#search-parameters") Set query string parameters]
li #[a(href="#search-parameters") 设置查询参数]
li #[a(href="#more-observables") Debounce search term input]
li #[a(href="#more-observables") 限制搜索框条目输入频率]
li #[a(href="#in-mem-web-api") Appendix: the in-memory web api service]
li #[a(href="#in-mem-web-api") 附录内存中的Web API服务]
p.
We illustrate these topics with code that you can
#[+liveExampleLink2('run live in a browser')].
p.
讲解这些主题的代码,可以在#[+liveExampleLink2('浏览器中运行在线版')].
.l-main-section
h1 Demos
h1 演示
p This chapter describes server communication with the help of the following demos
p 本章通过下面这些演示,描述了服务端通讯的用法。
ul
block demos-list
li #[a(href="#http-client") HTTP client: Tour of Heroes with Observables]
li #[a(href="#http-client") HTTP客户端: 使用可观察对象的《英雄指南》]
li #[a(href="#promises") HTTP client: Tour of Heroes with #{_Promise}s]
li #[a(href="#promises") HTTP客户端: 使用承诺的《英雄指南》]
li #[a(href="#cors") JSONP client: Wikipedia to fetch data from a service that does not support CORS]
li #[a(href="#cors") JSONP客户端: Wikipedia从一个不支持CORS的服务获取数据]
li #[a(href="#more-observables") JSONP client: Wikipedia using observable operators to reduce server calls]
li #[a(href="#more-observables") JSONP客户端: Wikipedia使用可观察对象的操作函数减少服务端调用]
:marked
These demos are orchestrated by the root `AppComponent`
这些演示由根组件`AppComponent`统一指挥
+makeExample('server-communication/ts/app/app.component.ts', null, 'app/app.component.ts')
block rxjs-import
:marked
There is nothing remarkable here _except_ for the import of RxJS operators.
这里唯一值得注意的是对RxJS操作函数的导入。
+makeExample('server-communication/ts/app/app.component.ts', 'import-rxjs')(format='.')
:marked
We'll talk about that [below](#rxjs) when we're ready to explore observables.
如果准备好学习可观察对象了,我们就[继续](#rxjs)。
:marked
First, we have to configure our application to use server communication facilities.
首先,我们必须配置应用程序才能使用服务器通讯设施。
.l-main-section
h1#http-providers Providing HTTP Services
h1#http-providers 提供HTTP服务
:marked
We use the !{_Angular_Http} client to communicate with a server using a familiar HTTP request/response protocol.
The `#{_Http}` client is one of a family of services in the !{_Angular_http_library}.
我们使用!{_Angular_Http}客户端来与服务器通讯它支持一组HTTP的请求(Request)/回应(Response)协议。
`#{_Http}`客户端是!{_Angular_http_libraryCn}所提供的服务大家庭中的一员。
block system-config-of-http
.l-sub-section
:marked
SystemJS knows how to load services from the !{_Angular_http_library} when we import from the `@angular/http` module
because we registered that module name in the `system.config` file.
当我们从`@angular/http`模块中导入服务时SystemJS知道该如何从!{_Angular_http_libraryCn}中加载它们,这是因为我们已经在`system.config`文件中注册过这个模块名。
:marked
Before we can use the `#{_Http}` client , we'll have to register it as a service provider with the Dependency Injection system.
要想使用`#{_Http}`客户端,我们得先通过依赖注入系统把它注册成一个服务供应商。
.l-sub-section
:marked
Learn about providers in the [Dependency Injection](dependency-injection.html) chapter.
了解关于供应商的更多知识,参见[依赖注入](dependency-injection.html)一章。
p In this demo, we register providers in the #[code bootstrap] method of #[code #[+adjExPath('app/main.ts')]].
p 下面这个演示中,我们在#[code #[+adjExPath('app/main.ts')]]文件的#[code bootstrap]方法中注册了供应商。
+makeExample('server-communication/ts/app/main.ts', 'v1', 'app/main.ts (v1)')(format='.')
block http-providers
:marked
We begin by importing the symbols we need, most of them familiar by now. The newcomer is `HTTP_PROVIDERS`,
a collection of service providers from the !{_Angular_http_library}.
我们从导入所需的符号开始,它们中的大多数我们都熟悉了,只有`HTTP_PROVIDERS`是新面孔,来自!{_Angular_http_libraryCn}的一组服务供应商。
We register HTTP providers in the bootstrap method by passing them in an array as the second parameter after the root component.
然后我们把这些HTTP服务供应商作为第二个参数传给bootstrap方法第一个参数用来指定根组件把它们注册进应用程序。
### Why register in *bootstrap*?
### 为什么在*bootstrap*中注册?
We prefer to register application-wide providers in the metadata `providers` array
of the root `AppComponent` like this:
要注册应用程序级的供应商,我们的首选方式把它放在根组件`AppComponent`元数据的`providers`数组中。就像这样:
+makeExample('server-communication/ts/app/app.component.ts','http-providers')(format='.')
:marked
Here we register the providers in the `bootstrap` method in the `main.ts` file. Why?
但这里我们却使用`main.ts`中的`bootstrap`方法注册了供应商。为什么呢?
This is a *sample application* that doesn't talk to a real server.
We're going to reconfigure the (typically-hidden) `XhrBackend` service with a fake provider
that fetches and saves sample data from an in-memory data store.
This replacement service is called the [*in-memory web api*](#in-mem-web-api).
因为这是一个*范例程序*,它不会跟真实的服务器打交道。
我们准备用一个伪造的供应商来重新配置(通常不可见的)`XhrBackend`服务。这个伪造的供应商会从一个内存存储区中获取和保存范例数据。
这个用于替换的服务称为[*内存Web API*](#in-mem-web-api)。
Such sleight-of-hand is something the root application component should *not* know about.
For this reason, and this reason *only*, we hide it *above* the `AppComponent` in `main.ts`.
这种“偷梁换柱”的把戏是应用程序的根组件*不应该*察觉的。
正因为这*一个*理由,我们把它藏在`main.ts`里,在层次上高于`AppComponent`。
.l-main-section
h1#http-client The Tour of Heroes #[i HTTP] Client Demo
h1#http-client 《英雄指南》#[i HTTP]客户端的演示
:marked
Our first demo is a mini-version of the [tutorial](../tutorial)'s "Tour of Heroes" (ToH) application.
This version gets some heroes from the server, displays them in a list, lets us add new heroes, and saves them to the server.
We use the !{_Angular_Http} client to communicate via `XMLHttpRequest (XHR)`.
我们的第一个演示是《英雄指南(TOH)》教程的一个mini版。
这个版本从服务器获取一些英雄,把它们显示在列表中,还允许我们添加新的英雄并将其保存到服务器。
我们借助!{_Angular_Http}客户端,来通过`XMLHttpRequest (XHR)`与服务器通讯。
It works like this.
它跑起来是这样的:
figure.image-display
img(src='/resources/images/devguide/server-communication/http-toh.gif' alt="ToH mini app" width="250")
:marked
This demo has a single component, the `HeroListComponent`. Here's its template:
这个范例是一个单一组件`HeroListComponent`,其模板如下:
+makeExample('server-communication/ts/app/toh/hero-list.component.html', null, 'app/toh/hero-list.component.html (template)')
:marked
It presents the list of heroes with an `ngFor`.
Below the list is an input box and an *Add Hero* button where we can enter the names of new heroes
and add them to the database.
We use a [template reference variable](template-syntax.html#ref-vars), `newHeroName`, to access the
value of the input box in the `(click)` event binding.
When the user clicks the button, we pass that value to the component's `addHero` method and then
clear it to make it ready for a new hero name.
它使用`ngFor`来展现这个英雄列表。
列表的下方是一个输入框和一个*Add Hero*按钮,在那里,我们可以输入新英雄的名字,并把他们加到数据库中。
我们在`(click)`事件绑定中使用[模板引用变量](template-syntax.html#ref-vars)`NewHeroName`来访问这个输入框的值。
当用户点击此按钮时,我们把这个值传给组件的`addHero`方法,然后清除它,以备输入新英雄的名字。
Below the button is an area for an error message.
按钮的下方是一个错误信息区。
a#oninit
a#HeroListComponent
:marked
## The *HeroListComponent* class
## *HeroListComponent*类
Here's the component class:
下面是这个组件类:
+makeExample('server-communication/ts/app/toh/hero-list.component.ts','component', 'app/toh/hero-list.component.ts (class)')
:marked
Angular [injects](dependency-injection.html) a `HeroService` into the constructor
and the component calls that service to fetch and save data.
Angular会把一个`HeroService`[注入](dependency-injection.html)到组件的构造函数中,该组件将调用此服务来获取和保存数据。
The component **does not talk directly to the !{_Angular_Http} client**!
The component doesn't know or care how we get the data.
It delegates to the `HeroService`.
这个组件**不会直接和!{_Angular_Http}客户端打交道**
它既不知道也不关心我们如何获取数据,这些都被委托给了`HeroService`去做。
This is a golden rule: **always delegate data access to a supporting service class**.
这是一条“黄金法则”:**总是把数据访问工作委托给一个提供支持的服务类**。
Although _at runtime_ the component requests heroes immediately after creation,
we do **not** call the service's `get` method in the component's constructor.
We call it inside the `ngOnInit` [lifecycle hook](lifecycle-hooks.html) instead
and count on Angular to call `ngOnInit` when it instantiates this component.
虽然_在运行期间_组件会在创建之后立刻请求这些英雄数据但我们**不会**在组件的构造函数中调用此服务的`get`方法。
而是在`ngOnInit`[生命周期钩子](lifecycle-hooks.html)中调用它Angular会在初始化该组件时调用`ngOnInit`方法。
.l-sub-section
:marked
This is a *best practice*.
Components are easier to test and debug when their constructors are simple and all real work
(especially calling a remote server) is handled in a separate method.
这是一项*最佳实践*。
当组件的构造函数足够简单,而且所有真实的工作(尤其是调用远端服务器)都在一个独立的方法中处理时,组件会更加容易测试和调试。
block getheroes-and-addhero
:marked
The service's `getHeroes()` and `addHero()` methods return an `Observable` of hero data that the !{_Angular_Http} client fetched from the server.
服务的`getHeroes()`和`addHero()`方法返回一个英雄数据的可观察对象(`Observable`),这些数据是由!{_Angular_Http}从服务器上获取的。
*Observables* are a big topic, beyond the scope of this chapter.
But we need to know a little about them to appreciate what is going on here.
*可观察对象(Observable)*是一个很大的主题,远超本章所能覆盖的范围之外。
但我们还是要了解它们一点儿,这样才能理解它在本章中做的那些工作。
We should think of an `Observable` as a stream of events published by some source.
We listen for events in this stream by ***subscribing*** to the `Observable`.
In these subscriptions we specify the actions to take when the web request
produces a success event (with the hero data in the event payload) or a fail event (with the error in the payload).
我们可以把可观察对象`Observable`看做一个由某些“源”发布的事件流。
我们通过***订阅***到可观察对象`Observable`来监听这个流中的那些事件。
在这些订阅中我们指定了当Web请求生成了一个成功事件(有效载荷是英雄数据)或失败事件(有效载荷是错误对象)时该如何采取行动。
:marked
With our basic intuitions about the component squared away, we're ready to look inside the `HeroService`.
关于组件的浅显讲解已经结束了,我们准备到`HeroService`的内部实现中看看。
.l-main-section
a#HeroService
h2#fetch-data Fetch data with the #[b HeroService]
h2#fetch-data 通过#[b HeroService]获取数据
:marked
In many of our previous samples we faked the interaction with the server by
returning mock heroes in a service like this one:
在前面的很多例子中,我们通过在服务中返回一个模拟的英雄列表来伪造了与服务器的交互过程。就像这样:
+makeExample('toh-4/ts/app/hero.service.ts', 'just-get-heroes')(format=".")
:marked
In this chapter, we revise that `HeroService` to get the heroes from the server using the !{_Angular_Http} client service:
在本章中,我们会修改`HeroService`,改用“!{_Angular_Http}客户端”服务来从服务器上获取英雄列表:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'v1', 'app/toh/hero.service.ts (revised)')
:marked
Notice that the !{_Angular_Http} client service is
[injected](dependency-injection.html) into the `HeroService` constructor.
注意,这个“!{_Angular_Http}客户端”服务[被注入](dependency-injection.html)到了`HeroService`的构造函数中。
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'ctor')
:marked
Look closely at how we call `#{_priv}http.get`
仔细看看我们是如何调用`#{_priv}http.get`的
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'http-get', 'app/toh/hero.service.ts (getHeroes)')(format=".")
:marked
We pass the resource URL to `get` and it calls the server which should return heroes.
我们把资源的URL传进`get`函数,它调用了服务器,而服务器应该返回英雄列表。
.l-sub-section
:marked
It *will* return heroes once we've set up the [in-memory web api](in-mem-web-api)
described in the appendix below.
Alternatively, we can (temporarily) target a JSON file by changing the endpoint URL:
一旦我们按附录中所描述的那样配置好了[内存Web API](in-mem-web-api),它*将*返回英雄列表。
但目前我们只能临时性的使用一个JSON文件来代替这个“内存Web API”。只要修改下服务器的URL就行了
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'endpoint-json')(format=".")
block rxjs
:marked
<a id="rxjs"></a>
The return value may surprise us.
Many of us who are familiar with asynchronous methods in modern JavaScript would expect the `get` method to return a
[promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
We'd expect to chain a call to `then()` and extract the heroes.
Instead we're calling a `map()` method.
Clearly this is not a promise.
返回值可能会让我们感到意外。
如果按照很多人在现代JavaScript中所熟悉的那种异步调用方法`get`方法应该返回一个
[承诺(promise)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
我们没有把一个函数调用链接进`then()`方法中,并从回调参数中取得英雄列表,而是调用了一个`map()`方法。
显然,这并不是承诺(Promise)。
In fact, the `http.get` method returns an **Observable** of HTTP Responses (`Observable<Response>`) from the RxJS library
and `map` is one of the RxJS *operators*.
事实上,`http.get`方法返回了一个HTTP Response类型的**可观察对象**(`Observable<Response>`)这个对象来自RxJS库而`map`是RxJS的*操作函数*之一。
.l-main-section
:marked
# RxJS Library
# RxJS库
[RxJS](https://github.com/ReactiveX/RxJS) ("Reactive Extensions") is a 3rd party library, endorsed by Angular,
that implements the [*asynchronous observable*](https://www.youtube.com/watch?v=UHI0AzD_WfY "Rob Wormald on observables") pattern.
[RxJS](https://github.com/ReactiveX/RxJS)("Reactive Extensions"的缩写)是一个被Angular认可的第三方库它实现了
[*异步可观察对象(asynchronous observable)*](https://www.youtube.com/watch?v=UHI0AzD_WfY "Rob Wormald on observables")模式。
All of our Developer Guide samples have installed the RxJS npm package and loaded via `system.js`
because observables are used widely in Angular applications.
We certainly need it now when working with the HTTP client.
And we must take a critical extra step to make RxJS observables usable.
这本开发指南中的所有例子都安装了RxJS的npm包而且都被`system.js`加载过了。这是因为可观察对象在Angular应用中使用非常广泛。
我们在使用HTTP客户端的时候当然就更需要它了。
不过仍然得经过一些额外的步骤才能让RxJS可观察对象在此处可用。
### Enable RxJS Operators
### 启用RxJS操作函数
The RxJS library is quite large.
Size matters when we build a production application and deploy it to mobile devices.
We should include only those features that we actually need.
RxJS库实在是太大了。
当我们构建一个产品级应用,并且把它发布到移动设备上的时候,大小就会成为一个问题。
我们应该只包含哪些我们确实需要的特性。
Accordingly, Angular exposes a stripped down version of `Observable` in the `rxjs/Observable` module,
a version that lacks most of the operators including some we'd like to use here
such as the `map` method we called above in `getHeroes`.
因此Angular在`rxjs/Observable`模块中导出了一个精简版的`Observable`类,这个版本缺少很多操作函数,
比如我们在上面的`getHeroes`方法中用过的`map`函数。
It's up to us to add the operators we need.
这让我们可以自由决定添加哪些操作函数。
We could add _every_ RxJS operators with a single import statement.
While that is the easiest thing to do, we'd pay a penalty in extended launch time and application size
because the full library is so big. We only use a few operators in our app.
我们确实可以把_每一个_RxJS操作函数都通过单一的import语句添加进去。
虽然这是最简单的方式,但我们也得付出代价,主要是在启动时间和应用大小上,因为完整的库实在太大了。
而我们其实只要用到少量操作函数。
Instead, we'll import each `Observable` operator and static class method, one-by-one, until we have a custom *Observable* implementation tuned
precisely to our requirements. We'll put the `import` statements in one `app/rxjs-operators.ts` file.
替代方案是,我们将一个一个的导入`Observable`的操作函数和静态类方法,直到我们得到了一个精确贴合我们需求的自定义*Observable*实现。
我们将把这些`import`语句放进一个`app/rxjs-operators.ts`文件里。
+makeExample('server-communication/ts/app/rxjs-operators.ts', null, 'app/rxjs-operators.ts')(format=".")
:marked
If we forget an operator, the TypeScript compiler will warn that it's missing and we'll update this file.
如果我们忘了导入某个操作函数TypeScript编译器就会警告说找不到它那时候我们再来更新此文件。
.l-sub-section
:marked
We don't need _all_ of these particular operators in the `HeroService` &mdash; just `map`, `catch` and `throw`.
We'll need the other operators later, in a *Wiki* example [below](#more-observables).
在`HeroService`中我们并不需要这里导入的_全部_操作函数 —— 我们只用到了`map`、`catch`和`throw`。
我们稍后的[*Wiki*例子](#more-observables)中,还会用到其它操作函数。
:marked
Finally, we import `rxjs-operator`_itself_ in our `app.component.ts`:
最后,我们把`rxjs-operator`_本身_导入`app.component.ts`文件中:
+makeExample('server-communication/ts/app/app.component.ts', 'import-rxjs', 'app/app.component.ts (import rxjs)')(format=".")
:marked
Let's return to our study of the `HeroService`.
让我们回来继续学习`HeroService`。
l-main-section
a#extract-data
:marked
## Process the response object
## 处理Response响应对象
Remember that our `getHeroes()` method mapped the `#{_priv}http.get` response object to heroes with an `#{_priv}extractData` helper method:
记住,我们的`getHeroes()`借助一个`#{_priv}extractData`辅助方法来把`#{_priv}http.get`的响应对象映射成了英雄列表:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'extract-data', 'app/toh/hero.service.ts (excerpt)')(format=".")
:marked
The `response` object does not hold our data in a form we can use directly.
To make it useful in our application we must parse the response data into a JSON object
这个`response`对象并没有使用我们能直接处理的形式保存数据。
要让它在应用程序中可用我们就必须把这个相应数据解析成一个JSON对象。
#### Parse to JSON
#### 解析成JSON
block parse-json
:marked
The response data are in JSON string form.
We must parse that string into JavaScript objects which we do by calling `response.json()`.
响应数据是JSON字符串格式的。
我们必须把这个字符串解析成JavaScript对象 —— 只要调一下`response.json()`就可以了。
.l-sub-section
:marked
This is not Angular's own design.
The Angular HTTP client follows the ES2015 specification for the
[response object](https://fetch.spec.whatwg.org/#response-class) returned by the `Fetch` function.
That spec defines a `json()` method that parses the response body into a JavaScript object.
这不是Angular自己的设计。
Angular HTTP客户端遵循了ES2015的[响应对象](https://fetch.spec.whatwg.org/#response-class)规范,它由`Fetch`函数返回。
此规范中定义了一个`json()`函数来把响应体解析成JavaScript对象。
.l-sub-section
:marked
We shouldn't expect the decoded JSON to be the heroes #{_array} directly.
The server we're calling always wraps JSON results in an object with a `data`
property. We have to unwrap it to get the heroes.
This is conventional web api behavior, driven by
[security concerns](https://www.owasp.org/index.php/OWASP_AJAX_Security_Guidelines#Always_return_JSON_with_an_Object_on_the_outside).
我们不能期待解码后的JSON直接就是一个英雄数组。
我们调用的这个服务器总会把JSON结果包装进一个带`data`属性的对象中。
我们必须解开它才能得到英雄数组。这是一个约定俗成的Web API行为规范它是出于
[安全方面的考虑](https://www.owasp.org/index.php/OWASP_AJAX_Security_Guidelines#Always_return_JSON_with_an_Object_on_the_outside)。
.alert.is-important
:marked
Make no assumptions about the server API.
Not all servers return an object with a `data` property.
不要对服务端API做任何假设。
并非所有服务器都会返回一个带`data`属性的对象。
:marked
### Do not return the response object
### 不要返回响应(Response)对象
Our `getHeroes()` could have returned the HTTP response. Bad idea!
The point of a data service is to hide the server interaction details from consumers.
The component that calls the `HeroService` wants heroes.
It has no interest in what we do to get them.
It doesn't care where they come from.
And it certainly doesn't want to deal with a response object.
我们的`getHeroes()`确实可以返回HTTP响应对象。但这可不是一个好主意
数据服务的重点在于,对消费者隐藏与服务器交互的细节。
调用`HeroService`的组件希望得到英雄数组。
它并不关心我们如何得到它们。
它也不在乎这些数据从哪里来。
毫无疑问,它也不希望直接和一个响应对象打交道。
+ifDocsFor('ts')
.callout.is-important
header HTTP GET is delayed
header HTTP的GET方法被推迟执行了
:marked
The `#{_priv}http.get` does **not send the request just yet!** This observable is
[*cold*](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/creating.md#cold-vs-hot-observables)
which means the request won't go out until something *subscribes* to the observable.
That *something* is the [HeroListComponent](#subscribe).
`#{_priv}http.get`**仍然没有发送请求!**这是因为可观察对象是
[*冷淡的*](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/creating.md#cold-vs-hot-observables)
这意味着,直到某人*订阅*了这个可观察对象时,这个请求才会被发出。
这个场景中的*某人*就是[HeroListComponent](#subscribe)。
a#error-handling
:marked
### Always handle errors
### 总是处理错误
Whenever we deal with I/O we must be prepared for something to go wrong as it surely will.
We should catch errors in the `HeroService` and do something with them.
We may also pass an error message back to the component for presentation to the user
but only if we can say something the user can understand and act upon.
一旦开始与I/O打交道我们就必须准备好接受墨菲定律如果一件倒霉事*可能*发生,它就*迟早会*发生。
我们可以在`HeroService`中捕获错误,并对它们做些处理。
也可以把错误信息传回到组件,让组件展示给最终用户,但只能用一些他们可以理解并照办的表达方式。
In this simple app we provide rudimentary error handling in both the service and the component.
在这个简单的应用中,我们在服务中和组件中都只提供了最原始的错误处理方式。
block error-handling
:marked
The eagle-eyed reader may have spotted our use of the `catch` operator in conjunction with a `handleError` method.
We haven't discussed so far how that actually works.
眼尖的读者可能注意会到,我们联合使用了`catch`操作函数和`handleError`方法。
但我们还没讨论过它实际的工作方式。
We use the Observable `catch` operator on the service level.
It takes an error handling function with an error object as the argument.
Our service handler, `handleError`, logs the response to the console,
transforms the error into a user-friendly message, and returns the message in a new, failed observable via `Observable.throw`.
我们在服务层使用了可观察对象的`catch`操作函数。
它接受一个以error对象为参数的错误处理函数。
我们的服务处理器(`handleError`)把响应对象记录到控制台中,
把错误转换成对用户友好的消息,并且通过`Observable.throw`来把这个消息放进一个新的、用于表示“失败”的可观察对象。
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'error-handling', 'app/toh/hero.service.ts (excerpt)')(format=".")
a#subscribe
a#hero-list-component
h4 #[b HeroListComponent] error handling
h4 #[b HeroListComponent] 错误处理
block hlc-error-handling
:marked
Back in the `HeroListComponent`, where we called `#{_priv}heroService.getHeroes()`,
we supply the `subscribe` function with a second function parameter to handle the error message.
It sets an `errorMessage` variable which we've bound conditionally in the `HeroListComponent` template.
回到`HeroListComponent`,这里我们调用了`#{_priv}heroService.getHeroes()`。我们提供了`subscribe`函数的第二个参数来处理错误信息。
它设置了一个`errorMessage`变量,我们把它有条件的绑定到了`HeroListComponent`模板中。
+makeExample('server-communication/ts/app/toh/hero-list.component.ts', 'getHeroes', 'app/toh/hero-list.component.ts (getHeroes)')(format=".")
.l-sub-section
:marked
Want to see it fail? Reset the api endpoint in the `HeroService` to a bad value. Remember to restore it!
想看到它失败时的情况吗?在`HeroService`中把API的端点设置为一个无效值就行了。但别忘了恢复它。
<a id="update"></a>
<a id="post"></a>
.l-main-section
:marked
## Send data to the server
## 往服务器发送数据
So far we've seen how to retrieve data from a remote location using an HTTP service.
Let's add the ability to create new heroes and save them in the backend.
前面我们已经看到如何用一个HTTP服务从远端获取数据了。
但我们还能再给力一点,让它可以创建新的英雄,并把他们保存到后端。
We'll create an easy method for the `HeroListComponent` to call, an `addHero()` method that takes
just the name of a new hero:
我们将为`HeroListComponent`创建一个简单的`addHero()`方法,它将接受新英雄的名字:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'addhero-sig')(format=".")
:marked
To implement it, we need to know some details about the server's api for creating heroes.
要实现它我们得知道关于服务端API要如何创建英雄的一些细节。
[Our data server](#server) follows typical REST guidelines.
It expects a [`POST`](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5) request
at the same endpoint where we `GET` heroes.
It expects the new hero data to arrive in the body of the request,
structured like a `Hero` entity but without the `id` property.
The body of the request should look like this:
[我们的数据服务器](#server)遵循典型的REST指导原则。
它期待在和`GET`英雄列表的同一个端点上存在一个[`POST`](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5)请求。
它期待从请求体(body)中获得新英雄的数据,数据的结构和`Hero`对象相同,但是不带`id`属性。
请求体应该看起来像这样:
code-example(format="." language="javascript").
{ "name": "Windstorm" }
:marked
The server will generate the `id` and return the entire `JSON` representation
of the new hero including its generated id. The hero arrives tucked inside a response object
with its own `data` property.
服务器将生成`id`,并且返回新英雄的完整`JSON`形式包括这个生成的id。该英雄的数据被塞进一个响应对象的`data`属性中。
Now that we know how the API works, we implement `addHero()`like this:
现在我们知道了这个API如何工作就可以像这样实现`addHero()`了:
+ifDocsFor('ts')
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'import-request-options', 'app/toh/hero.service.ts (additional imports)')(format=".")
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'addhero', 'app/toh/hero.service.ts (addHero)')(format=".")
:marked
### Headers
### 请求头(Headers)
The `Content-Type` header allows us to inform the server that the body will represent JSON.
我们通过这个`Content-Type`头告诉服务器body是JSON格式的。
+ifDocsFor('ts')
:marked
[Headers](../api/http/Headers-class.html) are one of the [RequestOptions](../api/http/RequestOptions-class.html).
Compose the options object and pass it in as the *third* parameter of the `post` method, as shown above.
[Headers](../api/http/Headers-class.html)是[RequestOptions](../api/http/RequestOptions-class.html)中的一员。
可以把这些配置对象组合起来,并且传给`post`方法的*第三个*参数,就像前面见过的那样。
:marked
### Body
### 请求体(Body)
Despite the content type being specified as JSON, the POST body must actually be a *string*.
Hence, we explicitly encode the JSON hero content before passing it in as the body argument.
虽然内容的类型被指定为了JSON可POST的请求体实际上仍然是一个*字符串*。
因此我们需要显式的将英雄数据编码然后把它当做body参数传过去。
+ifDocsFor('ts')
.l-sub-section
:marked
We may be able to skip the `JSON.stringify` step in the near future.
用不了多久,我们就能跳过`JSON.stringify`这一步儿了。
:marked
### JSON results
### JSON结果
As with `getHeroes()`, we [extract the data](#extract-data) from the response using the
`#{_priv}extractData()` helper.
像`getHeroes()`中一样,我们可以使用`#{_priv}extractData()`辅助函数从响应中[提取出数据](#extract-data)。
block hero-list-comp-add-hero
:marked
Back in the `HeroListComponent`, we see that *its* `addHero()` method subscribes to the observable returned by the *service's* `addHero()` method.
When the data, arrive it pushes the new hero object into its `heroes` array for presentation to the user.
+makeExample('server-communication/ts/app/toh/hero-list.component.ts', 'addHero', 'app/toh/hero-list.component.ts (addHero)')(format=".")
block promises
h2#promises Fall back to Promises
:marked
Although the Angular `http` client API returns an `Observable<Response>` we can turn it into a
[Promise<Response>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) if we prefer.
It's easy to do and a promise-based version looks much like the observable-based version in simple cases.
.l-sub-section
:marked
While promises may be more familiar, observables have many advantages.
Don't rush to promises until you give observables a chance.
:marked
Let's rewrite the `HeroService` using promises , highlighting just the parts that are different.
+makeTabs(
'server-communication/ts/app/toh/hero.service.promise.ts,server-communication/ts/app/toh/hero.service.ts',
'methods, methods',
'app/toh/hero.service.promise.ts (promise-based), app/toh/hero.service.ts (observable-based)')
:marked
Converting from an observable to a promise is as simple as calling `toPromise(success, fail)`.
We move the observable's `map` callback to the first *success* parameter and its `catch` callback to the second *fail* parameter
and we're done!
Or we can follow the promise `then.catch` pattern as we do in the second `addHero` example.
Our `errorHandler` forwards an error message as a failed promise instead of a failed Observable.
The diagnostic *log to console* is just one more `then` in the promise chain.
We have to adjust the calling component to expect a `Promise` instead of an `Observable`.
+makeTabs(
'server-communication/ts/app/toh/hero-list.component.promise.ts, server-communication/ts/app/toh/hero-list.component.ts',
'methods, methods',
'app/toh/hero-list.component.promise.ts (promise-based), app/toh/hero-list.component.ts (observable-based)')
:marked
The only obvious difference is that we call `then` on the returned promise instead of `subscribe`.
We give both methods the same functional arguments.
.l-sub-section
:marked
The less obvious but critical difference is that these two methods return very different results!
The promise-based `then` returns another promise. We can keep chaining more `then` and `catch` calls, getting a new promise each time.
The `subscribe` method returns a `Subscription`. A `Subscription` is not another `Observable`.
It's the end of the line for observables. We can't call `map` on it or call `subscribe` again.
The `Subscription` object has a different purpose, signified by its primary method, `unsubscribe`.
Learn more about observables to understand the implications and consequences of subscriptions.
h2#cors Cross-origin requests: Wikipedia example
:marked
We just learned how to make `XMLHttpRequests` using the !{_Angular_Http} service.
This is the most common approach for server communication.
It doesn't work in all scenarios.
For security reasons, web browsers block `XHR` calls to a remote server whose origin is different from the origin of the web page.
The *origin* is the combination of URI scheme, hostname and port number.
This is called the [Same-origin Policy](https://en.wikipedia.org/wiki/Same-origin_policy).
.l-sub-section
:marked
Modern browsers do allow `XHR` requests to servers from a different origin if the server supports the
[CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) protocol.
If the server requires user credentials, we'll enable them in the [request headers](#headers).
:marked
Some servers do not support CORS but do support an older, read-only alternative called [JSONP](https://en.wikipedia.org/wiki/JSONP).
Wikipedia is one such server.
.l-sub-section
:marked
This [StackOverflow answer](http://stackoverflow.com/questions/2067472/what-is-jsonp-all-about/2067584#2067584) covers many details of JSONP.
:marked
### Search wikipedia
Let's build a simple search that shows suggestions from wikipedia as we type in a text box.
figure.image-display
img(src='/resources/images/devguide/server-communication/wiki-1.gif' alt="Wikipedia search app (v.1)" width="250")
block wikipedia-jsonp+
:marked
Wikipedia offers a modern `CORS` API and a legacy `JSONP` search API. Let's use the latter for this example.
The Angular `Jsonp` service both extends the `#{_Http}` service for JSONP and restricts us to `GET` requests.
All other HTTP methods throw an error because JSONP is a read-only facility.
As always, we wrap our interaction with an Angular data access client service inside a dedicated service, here called `WikipediaService`.
+makeExample('server-communication/ts/app/wiki/wikipedia.service.ts',null,'app/wiki/wikipedia.service.ts')
:marked
The constructor expects Angular to inject its `jsonp` service.
We register that service with `JSONP_PROVIDERS` in the [component below](#wikicomponent) that calls our `WikipediaService`.
<a id="query-parameters"></a>
:marked
### Search parameters
The [Wikipedia 'opensearch' API](https://www.mediawiki.org/wiki/API:Opensearch)
expects four parameters (key/value pairs) to arrive in the request URL's query string.
The keys are `search`, `action`, `format`, and `callback`.
The value of the `search` key is the user-supplied search term to find in Wikipedia.
The other three are the fixed values "opensearch", "json", and "JSONP_CALLBACK" respectively.
.l-sub-section
:marked
The `JSONP` technique requires that we pass a callback function name to the server in the query string: `callback=JSONP_CALLBACK`.
The server uses that name to build a JavaScript wrapper function in its response which Angular ultimately calls to extract the data.
All of this happens under the hood.
:marked
If we're looking for articles with the word "Angular", we could construct the query string by hand and call `jsonp` like this:
+makeExample('server-communication/ts/app/wiki/wikipedia.service.1.ts','query-string')(format='.')
:marked
In more parameterized examples we might prefer to build the query string with the Angular `URLSearchParams` helper as shown here:
+makeExample('server-communication/ts/app/wiki/wikipedia.service.ts','search-parameters','app/wiki/wikipedia.service.ts (search parameters)')(format=".")
:marked
This time we call `jsonp` with *two* arguments: the `wikiUrl` and an options object whose `search` property is the `params` object.
+makeExample('server-communication/ts/app/wiki/wikipedia.service.ts','call-jsonp','app/wiki/wikipedia.service.ts (call jsonp)')(format=".")
:marked
`Jsonp` flattens the `params` object into the same query string we saw earlier before putting the request on the wire.
<a id="wikicomponent"></a>
:marked
### The WikiComponent
Now that we have a service that can query the Wikipedia API,
we turn to the component that takes user input and displays search results.
+makeExample('server-communication/ts/app/wiki/wiki.component.ts', null, 'app/wiki/wiki.component.ts')
:marked
The `providers` array in the component metadata specifies the Angular `JSONP_PROVIDERS` collection that supports the `Jsonp` service.
We register that collection at the component level to make `Jsonp` injectable in the `WikipediaService`.
The component presents an `<input>` element *search box* to gather search terms from the user.
and calls a `search(term)` method after each `keyup` event.
The `search(term)` method delegates to our `WikipediaService` which returns an observable array of string results (`Observable<string[]`).
Instead of subscribing to the observable inside the component as we did in the `HeroListComponent`,
we forward the observable result to the template (via `items`) where the [async pipe](pipes.html#async-pipe)
in the `ngFor` handles the subscription.
.l-sub-section
:marked
We often use the [async pipe](pipes.html#async-pipe) in read-only components where the component has no need to interact with the data.
We couldn't use the pipe in the `HeroListComponent` because the "add hero" feature pushes newly created heroes into the list.
:marked
## Our wasteful app
Our wikipedia search makes too many calls to the server.
It is inefficient and potentially expensive on mobile devices with limited data plans.
### 1. Wait for the user to stop typing
At the moment we call the server after every key stroke.
The app should only make requests when the user *stops typing* .
Here's how it *should* work &mdash; and *will* work &mdash; when we're done refactoring:
figure.image-display
img(src='/resources/images/devguide/server-communication/wiki-2.gif' alt="Wikipedia search app (v.2)" width="250")
:marked
### 2. Search when the search term changes
Suppose the user enters the word *angular* in the search box and pauses for a while.
The application issues a search request for *Angular*.
Then the user backspaces over the last three letters, *lar*, and immediately re-types *lar* before pausing once more.
The search term is still "angular". The app shouldn't make another request.
### 3. Cope with out-of-order responses
The user enters *angular*, pauses, clears the search box, and enters *http*.
The application issues two search requests, one for *angular* and one for *http*.
Which response will arrive first? We can't be sure.
A load balancer could dispatch the requests to two different servers with different response times.
The results from the first *angular* request might arrive after the later *http* results.
The user will be confused if we display the *angular* results to the *http* query.
When there are multiple requests in-flight, the app should present the responses
in the original request order. That won't happen if *angular* results arrive last.
<a id="more-observables"></a>
## More fun with Observables
We can address these problems and improve our app with the help of some nifty observable operators.
We could make our changes to the `WikipediaService`.
But we sense that our concerns are driven by the user experience so we update the component class instead.
+makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', null, 'app/wiki/wiki-smart.component.ts')
:marked
We made no changes to the template or metadata, confining them all to the component class.
Let's review those changes.
### Create a stream of search terms
We're binding to the search box `keyup` event and calling the component's `search` method after each keystroke.
We turn these events into an observable stream of search terms using a `Subject`
which we import from the RxJS observable library:
+makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'import-subject')
:marked
Each search term is a string, so we create a new `Subject` of type `string` called `searchTermStream`.
After every keystroke, the `search` method adds the search box value to that stream
via the subject's `next` method.
+makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'subject')(format='.')
:marked
### Listen for search terms
Earlier, we passed each search term directly to the service and bound the template to the service results.
Now we listen to the *stream of terms*, manipulating the stream before it reaches the `WikipediaService`.
+makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'observable-operators')(format='.')
:marked
We wait for the user to stop typing for at least 300 milliseconds
([debounceTime](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/debounce.md)).
Only changed search values make it through to the service
([distinctUntilChanged](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/distinctuntilchanged.md)).
The `WikipediaService` returns a separate observable of string arrays (`Observable<string[]>`) for each request.
We could have multiple requests *in flight*, all awaiting the server's reply,
which means multiple *observables-of-strings* could arrive at any moment in any order.
The [switchMap](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md)
(formerly known as `flatMapLatest`) returns a new observable that combines these `WikipediaService` observables,
re-arranges them in their original request order,
and delivers to subscribers only the most recent search results.
The displayed list of search results stays in sync with the user's sequence of search terms.
.l-sub-section
:marked
We added the `debounceTime`, `distinctUntilChanged`, and `switchMap` operators to the RxJS `Observable` class
in `rxjs-operators` as [described above](#rxjs)
a#in-mem-web-api
.l-main-section
:marked
## Appendix: Tour of Heroes in-memory server
If we only cared to retrieve data, we could tell Angular to get the heroes from a `heroes.json` file like this one:
+makeJson('server-communication/ts/app/heroes.json', null, 'app/heroes.json')(format=".")
.l-sub-section
:marked
We wrap the heroes array in an object with a `data` property for the same reason that a data server does:
to mitigate the [security risk](http://stackoverflow.com/questions/3503102/what-are-top-level-json-arrays-and-why-are-they-a-security-risk)
posed by top-level JSON arrays.
:marked
We'd set the endpoint to the JSON file like this:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'endpoint-json')(format=".")
- var _a_ca_class_with = _docsFor === 'ts' ? 'a custom application class with' : ''
:marked
The *get heroes* scenario would work.
But we want to *save* data too. We can't save changes to a JSON file. We need a web API server.
We didn't want the hassle of setting up and maintaining a real server for this chapter.
So we turned to an *in-memory web API simulator* instead.
.l-sub-section
:marked
The in-memory web api is not part of the Angular core.
It's an optional service in its own `angular2-in-memory-web-api` library
that we installed with npm (see `package.json`) and
registered for module loading by SystemJS (see `systemjs.config.js`)
:marked
The in-memory web API gets its data from #{_a_ca_class_with} a `createDb()`
method that returns a map whose keys are collection names and whose values
are #{_array}s of objects in those collections.
Here's the class we created for this sample based on the JSON data:
+makeExample('server-communication/ts/app/hero-data.ts', null, 'app/hero-data.ts')(format=".")
:marked
Ensure that the `HeroService` endpoint refers to the web API:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'endpoint')(format=".")
:marked
Finally, we need to redirect client HTTP requests to the in-memory web API.
block redirect-to-web-api
:marked
This redirection is easy to configure because Angular's `http` service delegates the client/server communication tasks
to a helper service called the `XHRBackend`.
To enable our server simulation, we replace the default `XHRBackend` service with
the in-memory web API service using standard Angular provider registration techniques.
We initialize the in-memory web API with *seed data* from the mock hero dataset at the same time.
p Here is the revised (and final) version of the #[code #[+adjExPath('app/main.ts')]] demonstrating these steps.
+makeExample('server-communication/ts/app/main.ts', 'final', 'app/main.ts (final)')(format=".")
p See the full source code in the #[+liveExampleLink2()].