1142 lines
65 KiB
Plaintext
1142 lines
65 KiB
Plaintext
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` — 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.
|
||
|
||
回到`HeroListComponent`,我们看到*该组件的*`addHero()`方法中订阅了这个由*服务中*的`addHero()`方法返回的可观察对象。
|
||
当有数据到来时,它就会把这个新的英雄对象追加(push)到`heroes`数组中,以展现给用户。
|
||
+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
|
||
h2#promises 退化为承诺(Promise)
|
||
: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.
|
||
|
||
虽然Angular的`http`客户端API返回的是`Observable<Response>`类型的对象,但我们也可以把它转成
|
||
[Promise<Response>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)。
|
||
这很容易,并且在简单的场景中,一个基于承诺(Promise)的版本看起来很像基于可观察对象(Observable)的版本。
|
||
.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.
|
||
|
||
我们来使用承诺重写`HeroService`,要特别注意那些不同的部分。
|
||
+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)`.
|
||
|
||
把可观察对象转变成承诺是很简单的,只要调用`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.
|
||
|
||
我们把可观察对象的`map`回调移到第一个*success*参数中,而它的`catch`回调移到第二个*fail*参数中,这就算完工了。
|
||
或者我们可以像第二个`addHero`例子中那样使用承诺的`then.catch`模式。
|
||
|
||
Our `errorHandler` forwards an error message as a failed promise instead of a failed Observable.
|
||
|
||
我们的`errorHandler`也改用了一个失败的承诺,而不再是失败的可观察对象。
|
||
|
||
The diagnostic *log to console* is just one more `then` in the promise chain.
|
||
|
||
把诊断信息*记录到控制台*也只是在承诺的处理链中多了一个`then`而已。
|
||
|
||
We have to adjust the calling component to expect a `Promise` instead of an `Observable`.
|
||
|
||
为了用`Observable`代替`Promise`,我们还得对调用方组件进行调整。
|
||
|
||
+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.
|
||
|
||
唯一一个比较明显的不同点是我们调用这个返回的承诺的`then`方法,而不再是`subscribe`。
|
||
而且我们给了这两个方法完全相同的调用参数。
|
||
.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.
|
||
|
||
基于承诺的`then`返回了另一个承诺。我们可以链式调用多个`then`和`catch`方法,每次都返回一个新的承诺。
|
||
|
||
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`.
|
||
|
||
但`subscribe`方法返回一个`Subscription`对象。但`Subscription`不是另一个`Observable`。
|
||
它是可观察对象的末端。我们不能在它上面调用`map`函数或再次调用`subscribe`函数。
|
||
`Subscription`对象的设计目的是不同的,这从它的主方法`unsubscribe`就能看出来。
|
||
|
||
Learn more about observables to understand the implications and consequences of subscriptions.
|
||
|
||
请学习更多关于可观察对象的知识,来理解订阅的实现和效果。
|
||
|
||
h2#cors Cross-origin requests: Wikipedia example
|
||
h2#cors 跨域请求:Wikipedia范例
|
||
: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.
|
||
|
||
我们刚刚学习了用!{_Angular_Http}服务发起`XMLHttpRequests`。
|
||
这是与服务器通讯时最常用的方法。
|
||
但它不适合所有场景。
|
||
|
||
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).
|
||
|
||
出于安全的考虑,Web浏览器会阻止到与当前页面不“同源”的远端服务器的`XHR`调用。
|
||
所谓*源*就是URI的协议(scheme)、主机名(host)和端口号(port)这几部分的组合。
|
||
这被称为[同源策略](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).
|
||
|
||
在现代浏览器中,如果服务器支持[CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)协议,那么也可以向不同源的服务器发起`XHR`请求。
|
||
如果服务器要请求用户凭证,我们就在[请求头](#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.
|
||
|
||
有些服务器不支持CORS,但支持一种老的、只读的(译注:即仅支持GET)替代协议,那就是[JSONP](https://en.wikipedia.org/wiki/JSONP)。
|
||
Wikipedia就是一个这样的服务器。
|
||
.l-sub-section
|
||
:marked
|
||
This [StackOverflow answer](http://stackoverflow.com/questions/2067472/what-is-jsonp-all-about/2067584#2067584) covers many details of JSONP.
|
||
|
||
这个[StackOverflow上的答案](http://stackoverflow.com/questions/2067472/what-is-jsonp-all-about/2067584#2067584)覆盖了关于JSONP的很多细节。
|
||
:marked
|
||
### Search wikipedia
|
||
### 搜索Wikipedia
|
||
|
||
Let's build a simple search that shows suggestions from wikipedia as we type in a text box.
|
||
|
||
我们来构建一个简单的搜索程序,当我们在文本框中输入时,它会从Wikipedia中获取并显示建议的词汇列表。
|
||
|
||
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.
|
||
|
||
Wikipedia提供了一个现代的`CORS` API和一个传统的`JSONP`搜索API。在这个例子中,我们使用后者。
|
||
Angular的`Jsonp`服务既通过JSONP扩展了`#{_Http}`服务,又限制了我们只能用`GET`请求。
|
||
尝试调用所有其它HTTP方法都将抛出一个错误,这是因为JSONP是只读的。
|
||
|
||
As always, we wrap our interaction with an Angular data access client service inside a dedicated service, here called `WikipediaService`.
|
||
|
||
像往常一样,我们把和Angular数据访问服务进行交互的代码全都封装在一个专门的服务中。我们称之为`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`.
|
||
|
||
这个构造函数期望Angular给它注入一个`jsonp`服务。
|
||
我们在[下面这个组件](#wikicomponent)(它会调用`WikipediaService`服务)中,用`JSONP_PROVIDERS`注册了这个服务。
|
||
|
||
<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.
|
||
|
||
[Wikipedia 的 'opensearch' API](https://www.mediawiki.org/wiki/API:Opensearch)期待在所请求的URL中带四个查询参数(键/值对格式)。
|
||
这些键(key)分别是`search`、`action`、`format`和`callback`。
|
||
`search`的值是一个用户提供的打算在Wikipedia中查找的关键字。
|
||
另外三个参数是固定值,分别是"opensearch"、"json"和"JSONP_CALLBACK"。
|
||
.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.
|
||
|
||
`JSONP`技术需要我们通过查询参数传给服务器一个回调函数的名字:`callback=JSONP_CALLBACK`。
|
||
服务器使用这个名字在它的响应体中构建一个JavaScript包装函数,Angular最终会调用这个包装函数来提取出数据。
|
||
这些都是Angular在背后默默完成的,你不会感受到它。
|
||
:marked
|
||
If we're looking for articles with the word "Angular", we could construct the query string by hand and call `jsonp` like this:
|
||
|
||
如果我们要找那些含有关键字“Angular”的文档,我们可以先手工构造出查询字符串,并像这样调用`jsonp`:
|
||
+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:
|
||
|
||
在更为参数化的例子中,我们会首选Angular的`URLSearchParams`辅助类来构建查询字符串,就像这样:
|
||
+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.
|
||
|
||
这次我们使用了*两个*参数来调用`jsonp`:`wikiUrl`和一个配置对象,配置对象的`search`属性是刚构建的这个`params`对象。
|
||
+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.
|
||
|
||
`Jsonp`把`params`对象平面化为一个查询字符串,而这个查询字符串和以前我们直接放在请求中的那个是一样的。
|
||
|
||
<a id="wikicomponent"></a>
|
||
:marked
|
||
### The WikiComponent
|
||
### 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.
|
||
|
||
现在,我们有了一个可用于查询Wikpedia API的服务,
|
||
我们重新回到组件中,接收用户输入,并显示搜索结果。
|
||
|
||
+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`.
|
||
|
||
该组件元数据中的`providers`数组指定了Angular的`JSONP_PROVIDERS`集合,以支持`Jsonp`服务。
|
||
我们把这个集合注册在组件级,以便`Jsonp`可以被注入进`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.
|
||
|
||
该组件把一个`<input>`元素作为*搜索框*作为从用户那里手机搜索关键字的入口。
|
||
并在每次`keyup`事件被触发时,调用`search(term)`方法。
|
||
|
||
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.
|
||
|
||
`search(term)`方法委托我们的`WikipediaService`服务来完成实际操作。该服务返回的是一个字符串数组的可观察对象(`Observable<string[]`)。
|
||
该组件的内部订阅了这个可观察对象,就像我们曾在`HeroListComponent`中所做的那样,
|
||
我们把这个可观察对象作为结果传给模板(通过`items`属性),模板中`ngFor`上的[async(异步)管道](pipes.html#async-pipe)会对这个订阅进行处理。
|
||
.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.
|
||
|
||
我们通常在只读组件中使用[async管道](pipes.html#async-pipe),这种组件不需要与数据进行互动。
|
||
但我们不能在`HeroListComponent`中使用这个管道,这是因为“添加新英雄”特性会把一个新创建的英雄追加到英雄列表中。
|
||
: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.
|
||
|
||
我们这个Wikipedia搜索程序触发了过多的服务器调用(每次按键发一次)。
|
||
这样效率很低,而且在流量受限的移动设备上会显得过于昂贵。
|
||
|
||
### 1. Wait for the user to stop typing
|
||
### 1. 等用户停止输入
|
||
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 — and *will* work — 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
|
||
### 2. 当搜索关键字变化了才搜索
|
||
|
||
Suppose the user enters the word *angular* in the search box and pauses for a while.
|
||
The application issues a search request for *Angular*.
|
||
|
||
假设用户在输入框中输入了单词*angular*,然后稍等片刻。
|
||
应用程序就会发出一个对*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.
|
||
|
||
然后,用户用退格键删除了最后三个字符*lar*,并且毫不停顿的重新输入了*lar*。
|
||
搜索关键词仍然是“angular”。这时应用程序不应该发起另一个请求。
|
||
|
||
### 3. Cope with out-of-order responses
|
||
### 3. 对付乱序响应体
|
||
|
||
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*.
|
||
|
||
用户输入了*angular*,暂停,清除搜索框,然后输入*http*。
|
||
应用程序发起了两个搜索请求,一个搜*angular*,一个搜*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.
|
||
|
||
哪一个响应会先回来?我们是没法保证的。
|
||
负载均衡器可能把这个请求分发给了响应时间不同的两台服务器。
|
||
来自第一个*angular*请求的结果可能晚于稍后的*http*的结果。
|
||
用户可能会困惑:为什么在*http*请求中显示了*angular*的结果。
|
||
|
||
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.
|
||
|
||
如果有多个请求“打架”,应用程序应该按照原始请求的顺序展示对它们的响应。即使*angular*的结果后返回,也不会发生这样的混乱。
|
||
|
||
<a id="more-observables"></a>
|
||
## More fun with Observables
|
||
## Observable的更多乐趣
|
||
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.
|
||
|
||
我们本可以把这些改动合并进`WikipediaService`中,但别忘了我们的关注点应该是由用户体验驱动的,所以,还是把它放到组件类中吧。
|
||
|
||
+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
|
||
### 创建一个有搜索关键字组成的“流(Stream)”
|
||
|
||
We're binding to the search box `keyup` event and calling the component's `search` method after each keystroke.
|
||
|
||
我们正在绑定到搜索框的`keyup`事件,并且在每次按键之后调用组件的`search`方法。
|
||
|
||
We turn these events into an observable stream of search terms using a `Subject`
|
||
which we import from the RxJS observable library:
|
||
|
||
我们借助`Subject`类把这些事件转变成一个由搜索关键字组成的可观察的“流”对象。`Subject`是我们从RxJS库中导入的:
|
||
+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.
|
||
|
||
每个搜索关键字都是一个字符串,所以我们创建了一个新的`string`类型的`Subject`,并把它称为`searchTermStream`。
|
||
在每次按键之后,`search`方法都会通过`Subject`的`next`方法把搜索框的值添加到流中。
|
||
+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`.
|
||
|
||
以前,我们每次都把搜索关键字直接传给服务,并且把模板绑定到服务返回的结果。
|
||
而现在我们在监听*关键字组成的流*,并在把它传给`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)).
|
||
|
||
我们先等待用户停止输入至少300毫秒
|
||
([debounceTime](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/debounce.md))。
|
||
只有当搜索关键字变化的时候,才把它传给服务
|
||
([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.
|
||
|
||
`WikipediaService`服务为每个请求返回一个独立的可观察的字符串数组(`Observable<string[]>`)。
|
||
我们可以同时有多个*发送中*的请求,它们都在等服务器的回复,
|
||
这意味着多个*可观察的字符串数组*有可能在任何时刻以任何顺序抵达。
|
||
|
||
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.
|
||
|
||
[switchMap](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md)(常被称为`flatMapLatest`)
|
||
返回一个新的可观察对象,它组合了所有这些“可观察的字符串数组”,重新按照它们的原始请求顺序进行排列,然后把最近的一个搜索结果交付给调用者。
|
||
|
||
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)
|
||
|
||
在[前面提过的](#rxjs)`rxjs-operators`文件中,我们把`debounceTime`、`distinctUntilChanged`和`switchMap`操作符加到了RxJS的`Observable`类中。
|
||
|
||
a#in-mem-web-api
|
||
.l-main-section
|
||
:marked
|
||
## Appendix: Tour of Heroes in-memory server
|
||
## 附录:《英雄指南》的内存(in-memory)服务器
|
||
|
||
If we only cared to retrieve data, we could tell Angular to get the heroes from a `heroes.json` file like this one:
|
||
|
||
如果我们只关心获取到的数据,我们可以告诉Angular从一个从一个`heroes.json`文件中获取英雄列表,就像这样:
|
||
+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.
|
||
|
||
我们把英雄数组包装在一个带有`data`属性的对象中,就像一个数据服务器所做的那样:
|
||
用于缓解由顶层的JSON数组带来的[安全风险](http://stackoverflow.com/questions/3503102/what-are-top-level-json-arrays-and-why-are-they-a-security-risk)。
|
||
:marked
|
||
We'd set the endpoint to the JSON file like this:
|
||
|
||
我们要像这样把端点设置为这个JSON文件:
|
||
+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.
|
||
|
||
这在*“获取”英雄数据*的场景下确实能工作,但我们还想*保存*数据。我们不能把这些改动保存到JSON文件中,我们需要一个Web API服务器。
|
||
在本章中,我们不想惹上配置和维护真实服务器的那些麻烦事。
|
||
所以,我们转而使用一种*内存Web API仿真器*代替它。
|
||
|
||
.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`)
|
||
|
||
内存Web API不是Angular内核的一部分。
|
||
它是一个可选的服务,来自独立的`angular2-in-memory-web-api`库。我们可以通过npm(参见`package.json`)来安装它,
|
||
并且通过SystemJS(参见`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.
|
||
|
||
内存Web API从一个带有`createDb()`方法的自定义类中获取数据,并且返回一个map,它的主键(key)是一组名字,而值(value)是一组与之对应的对象数组。
|
||
|
||
Here's the class we created for this sample based on the JSON data:
|
||
|
||
这里是与范例中那个基于JSON的数据源完成相同功能的类:
|
||
+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:
|
||
|
||
确保`HeroService`的端点指向了这个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.
|
||
|
||
最后,我们需要把来自HTTP客户端的请求重定向到这个内存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`.
|
||
|
||
这次重定向非常易于配置,这是因为Angular的`http`服务把客户端/服务器通讯的工作委托给了一个叫做`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.
|
||
|
||
要想启用我们的服务模拟器,我们通过Angular标准的“供应商注册”技术,把默认的`XHRBackend`服务替换为了这个内存Web API服务。
|
||
同时,我们使用来自模拟的英雄数据集的*种子数据*初始化了这个内存Web API。
|
||
|
||
p Here is the revised (and final) version of the #[code #[+adjExPath('app/main.ts')]] demonstrating these steps.
|
||
|
||
p 下面是修改过的(也是最终的)#[code #[+adjExPath('app/main.ts')]]版本,用于演示这些步骤。
|
||
+makeExample('server-communication/ts/app/main.ts', 'final', 'app/main.ts (final)')(format=".")
|
||
|
||
p See the full source code in the #[+liveExampleLink2()].
|
||
p 要想查看完整的源代码,参见#[+liveExampleLink2('浏览器中运行在线版')]。
|