angular-cn/public/docs/ts/latest/guide/server-communication.jade

506 lines
28 KiB
Plaintext
Raw Normal View History

include ../../../../_includes/_util-fns
:marked
Most applications read from or write to a remote server via HTTP or JSONP.
The Angular 2 http client supports both as we learn in this chapter covering:
[Http client sample overview](#http-client)<br>
[Fetch data with http.get](#fetch-data)<br>
[Enable RxJS Operators](#enable-rxjs-operators)<br>
[Extract JSON data with RxJS map](#map)<br>
[Error handling](#error-handling)<br>
[Log results to console](#do)<br>
[Send data to the server](#update)<br>
[Promises instead of observables](#promises)<br>
[JSONP](#jsonp)<br>
[Debounce search term input](#more-observables)<br>
[Appendix: the in-memory web api service](#in-mem-web-api)<br>
Try the [live example](/resources/live-examples/server-communication/ts/plnkr.html).
.l-main-section
:marked
## The Basics
When we look at the broad task of connecting multiple devices to talk to each other, there really are plenty of different options.
If we zoom in a little and restrict us to only look at the options that allow a web application that runs in a browser to communicate with
a backend we are basically left with communication via the [`HTTP`](https://tools.ietf.org/html/rfc2616) or [`WebSocket`](https://tools.ietf.org/html/rfc6455) protocol.
For web apps to communicate via `HTTP` there are basically two different techniques which are `XMLHttpRequest (XHR)` and `JSONP`.
They are quite different from each other both in purpose and implementation.
Angular comes with its own built-in abstractions that allow us to exploit both techniques in a nicely well integrated way.
## Http Client
We use the Angular `Http` client to communicate via `XMLHttpRequest (XHR)`.
We'll illustrate with a mini-version of the [tutorial](../tutorial)'s "Tour of Heroes" (ToH) application.
This one gets some heroes from the server, displays them in a list, lets us add new heroes, and save them to the server.
It works like this.
figure.image-display
img(src='/resources/images/devguide/server-communication/http-toh.gif' alt="ToH mini app" width="250")
:marked
It's implemented with two components &mdash; a parent `TohComponent` shell and the `HeroListComponent` child.
We've seen these kinds of component in many other documentation samples.
Let's see how they change to support communication with a server.
.l-sub-section
:marked
We're overdoing the "separation of concerns" by creating two components for a tiny demo.
We're making a point about application structure that is easier to justify when the app grows.
:marked
Here is the `TohComponent` shell:
+makeExample('server-communication/ts/app/toh/toh.component.ts', null, 'app/toh.component.ts')
:marked
As usual, we import the symbols we need. The newcomer is `HTTP_PROVIDERS`,
an array of service providers from the Angular HTTP library.
We'll be using that library to access the server.
We also import a `HeroService` that we'll look at shortly.
The component specifies both the ``HTTP_PROVIDERS` and the `HeroService` in the metadata `providers` array,
making them available to the child components of this "Tour of Heroes" application.
.l-sub-section
:marked
Learn about providers in the [Dependency Injection](dependency-injection.html) chapter.
:marked
This sample only has one child, the `HeroListComponent` shown here in full:
+makeExample('server-communication/ts/app/toh/hero-list.component.ts', null, 'app/toh/hero-list.component.ts')
:marked
The component template displays a list of heroes with the `NgFor` repeater directive.
Beneath the heroes 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 [local template variable](template-syntax.html#local-vars), `newHero`, 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 ready for a new hero name.
Below the button is an optional error message.
### The `HeroListComponent` class
We [inject](dependency-injection.html) the `HeroService` into the constructor.
That's the instance of the `HeroService` that we provided in the parent shell `TohComponent`.
Notice that the component **does not talk to the server directly!**.
The component doesn't know or care how we get the data.
Those details it delegates to the `heroService` class (which we'll get to in a moment).
This is a golden rule: *always delegate data access to a supporting service class*.
Although the component should request heroes immediately,
we do **not** call the service `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.
.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.
:marked
The service `get` and `addHero` methods return an `Observable` to which we `subscribe`,
specifying the actions to take if a method succeeds or fails.
We'll get to observables and subscription shortly.
With our basic intuitions about the component squared away, we can turn to development of the backend data source
and the client-side `HeroService` that talks to it.
### Fetch data
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 get the heroes from the server using Angular's own HTTP Client service.
Here's the new `HeroService`:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'v1', 'app/toh/hero.service.ts')(format=".")
:marked
We begin by importing Angular's `Http` client service and
[inject it](dependency-injection.html) into the `HeroService` constructor.
`Http` is not part of the Angular core. It's an optional service in its own `angular2/http` library.
Moreover, this library isn't even part of the main Angular script file.
It's in its own script file (included in the Angular npm bundle) which we must load in `index.html`.
+makeExample('server-communication/ts/index.html', 'http', 'index.html')(format=".")
:marked
Look closely at how we call `http.get`
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'http-get-v1', 'app/toh/hero.service.ts (http.get)')(format=".")
:marked
We pass the resource URL to `get` and it calls the server which returns
data from the `heroes.json` file.
The return value may surprise us. Many of us would expect 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.
### RxJS Observables
The `http.get` method returns an **Observable** from the [RxJS library](https://github.com/ReactiveX/RxJS).
.l-sub-section
:marked
We cover the rudiments of RxJS in the [Observables](observables.html) chapter.
:marked
RxJS is a 3rd party library endorsed by Angular.
All of our documenations samples have installed the RxJS npm package and loaded the script in `index.html`
because observables are used widely in Angular applications.
+makeExample('server-communication/ts/index.html', 'rxjs', 'index.html')(format=".")
:marked
We certainly need it now when working with the HTTP client.
And we must take a critical extra step to make RxJS observables usable.
### Enable RxJS Operators
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.
Accordingly, Angular exposes a stripped down version of `Observable` in the `rxjs/Observable` module,
a version that lacks almost all operators including the ones we'd like to use here
such as the `map` method we called above in `getHeroes`.
It's up to us to add the operators we need.
We could add each operator, one-by-one, until we had a custom *Observable* implementation tuned
precisely to our requirements.
That would be a distraction today. We're learning HTTP, not counting bytes.
So we'll make it easy on ourselves and enrich *Observable* with the full set of operators.
It only takes one `import` statement.
It's best to add that statement early when we're bootstrapping the application.
:
+makeExample('server-communication/ts/app/main.ts', 'import-rxjs', 'app/main.ts (import rxjs)')(format=".")
:marked
<a id="map"></a>
### Map the response object
Let's come back to the `HeroService` and look at the `http.get` call again to see why we needed `map()`
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'http-get-v1', 'app/toh/hero.service.ts (http.get)')(format=".")
:marked
The `response` object does not hold our data in a form we can use directly.
It takes an additional step &mdash; calling `response.json()` &mdash; to transform the bytes from the server into a JSON object.
.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.
We must also know the shape of the data returned by the server API.
We shouldn't expect `json()` to return 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.
:marked
### Do not return the response object
Our `getHeroes()` could have returned returned the `Observable<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.
<a id="error-handling"></a>
### Always handle errors
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.
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.
In this simple app we provide rudimentary error handling in both the service and the component.
We use the Observable `catch` operator on the service level.
It takes an error handling function with the failed `Response` object as the argument.
Our service handler, `errorHandler`, 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`.
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'error-handling', 'app/toh/hero.service.ts')(format=".")
:marked
Back in the `HeroListComponent`, where we called `heroService.get`,
we supply the `subscribe` function with a second function to handle the error message.
It sets an `errorMessage` variable which we've bound conditionally in the template.
+makeExample('server-communication/ts/app/toh/hero-list.component.ts', 'getHeroes', 'app/toh/hero-list.component.ts (getHeroes)')(format=".")
.l-sub-section
:marked
Try messing with the value of the api endpoint in the `HeroService` and watch it fail
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'endpoint', 'app/toh/hero.service.ts (endpoint)')
<a id="do"></a>
<a id="console-log"></a>
:marked
### Peek at results in the console
During development we're often curious about the data returned by the server.
Logging to console without disrupting the flow would be nice.
The Observable `do` operator is perfect for the job.
It passes the input through to the output while we do something with a useful side-effect such as writing to console.
Slip it into the pipeline between `map` and `catch` like this.
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'http-get', 'app/toh/hero.service.ts')(format=".")
:marked
Remember to comment it out before going to production!
<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 Angular's built-in `Http` service.
Let's add the ability to create new heroes and save them in the backend.
We'll create an easy method for the `HeroListComponent` to call, an `addHero` method that takes
just the name of a new hero and returns an observable holding the newly-saved hero:
code-example(format="." language="javascript").
addHero (name: string) : Observable&lt;Hero>
:marked
To implement it, we need to know some details about the server's api for creating heroes.
[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:
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 for our convenience.
Now that we know how the API works, we implement `addHero`like this:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'addhero', 'app/toh/hero.service.ts (addHero)')(format=".")
:marked
Notice that the second *body* parameter of the `post` method requires a JSON ***string***
so we have to `JSON.stringify` the hero content first.
.l-sub-section
:marked
We may be able to skip stringify in the near future.
: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=".")
<a id="promises"></a>
:marked
## Fall back to Promises
Although the Angular `http` client API returns an `Observable` we can turn it into a
[Promise](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've 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.1.ts,server-communication/ts/app/toh/hero.service.ts',
'methods, methods',
'app/toh/hero.service.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.
We have to adjust the calling component to expect a `Promise` instead of an `Observable`.
+makeTabs(
'server-communication/ts/app/toh/hero-list.component.1.ts, server-communication/ts/app/toh/hero-list.component.ts',
'methods, methods',
'app/toh/hero-list.component.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.
<a id="jsonp"></a>
:marked
## Get data with `JSONP`
We just learned how to make `XMLHttpRequests` using Angulars built-in `Http` service. This is the most common approach for
server communication. It doesn't work in all scenarios though.
For security reasons web browser do not permit to make `XHR` calls if the origin of the remote server is different from the one the web page runs in.
The origin is defined as the combination of URI scheme, hostname and port number. The policy that prevents such `XHR` requests is called [Same-origin Policy](https://en.wikipedia.org/wiki/Same-origin_policy) accordingly.
.l-sub-section
:marked
That's not entirely true. Modern browsers do allow `XHR` requests against foreign origins if the server sends the appropriate [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) headers.
In such cases there's nothing specific to do for the client unless we want our `XHR` request to include credentials (e.g Cookies) which we then have to explicitly enable for the request.
:marked
But let's not go too deep into the rabbit hole of `JSONP`. If you like to learn how this technique works under the cover, you may like to read up on this [answer](http://stackoverflow.com/questions/2067472/what-is-jsonp-all-about/2067584#2067584) on StackOverflow.
For now, let's focus on how we can use `JSONP` in Angular.
### Search wikipedia
Wikipedia offers a `JSONP` search api. 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")
:marked
Angular provides us with a `Jsonp` services which has the same API surface as the `Http` service with the only difference that it restricts us to use `GET` requests only. This is
because the nature of `JSONP` does not allow any other kind of requests. In order to use the `Jsonp` service we have to specify the `JSONP_PROVIDERS`.
Again, we'll make sure to do the server communication in a dedicated service that we call `WikipediaService`.
+makeExample('server-communication/ts/app/wiki/wikipedia.service.ts',null,'app/wiki/wikipedia.service.ts')
:marked
We use the `URLSearchParams` helper to define a `params` object with the key/value pairs that define the wikipedia query.
The keys are `search`, `action`, and `format`.
The value of the `search` key is the user-supplied search term that we'll lookup in wikipedia.
We call `Jsonp` with two arguments: the `wikiUrl` and an options object with a `search` property whose value is the `params` object.
`Jsonp` flattens the `params` object into a query string such as
code-example.
&search=foo&action=opensearch&format=json`
:marked
and appends it to the `wikiUrl`. Notice that the `wikiUrl` contains `callback=JSONP_CALLBACK`. The nature of `JSONP` requires that
we pass a unique function name to the server so that the server can pick up that name in the response. Many JSONP APIs pass this function name
through an URL parameter called they call `callback`. But that's more common sense than a strict rule. Since Angular doesn't assume a fixed name
for that parameter it requires *us* to put it in the URL and asign the placeholder `JSONP_CALLBACK` to it. Angular will make sure to replace
the placeholder with a unique function name for each request for us.
Now that we have the service ready to query the Wikipedia API, let's look at the actual component that should accept the user input
and shows the search results.
+makeExample('server-communication/ts/app/wiki/wiki.component.ts', null, 'app/wiki/wiki.component.ts')
:marked
There shouldn't be much of a surprise here. Our component defines an `<input>` and calls a `search(term)` method for each `keyup` event.
The `search(term)` method delegates to our `WikipediaService` which returns an `Observable<Array<string>>` to us. Instead of subscribing to it
manually we use the [async pipe](pipes.html#async-pipe) in the `ngFor` to let the view subscribe to the Observable directly.
.l-sub-section
:marked
As a rule of thumb we can use the [async pipe](pipes.html#async-pipe) whenever we don't need to interact with the unwrapped payload from the `Observable`/`Promise`
other than from the view directly. In our previous example we couldn't use it since we needed to keep a reference to the bare array of heros so that we can push
new objects into the array as we create new heros.
:marked
There are a bunch of things in our wikipedia demo that we could do better. This is a perfect opportunity to show off some nifty
`Observable` tricks that can make server communication much simpler and more fun.
<a id="more-observables"></a>
### More fun with Observables
If you ever wrote a search-as-you-type control yourself before, you are probably aware of some typical corner cases that arise with this task.
### 1. Don't hit the search endpoint on every key stroke
Treat the search endpoint as if you pay for it on a per-request basis. No matter if it's your own hardware or not. We shouldn't be hammering
the search enpoint more often than needed.
What we want is to hit the search endpoint as soon as the user *stops typing* instead of after every keystroke.
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. Don't hit the search endpoint for the same term again
Consider you type *foo*, stop, type another *o*, hit return and stop back at *foo*. That should be just one request with the term *foo*
and not two even if we technically stopped twice after we had *foo* in the search box.
### 3. Cope with out-of-order responses
When we have multiple requests in-flight at the same time we must account for cases where they come back in unexpected order. Consider we first typed
*computer*, stop, a request goes out, we type *car*, stop, a request goes out but then the request that carries the results for *computer* comes back
after the request that carries the results for *car*. If we don't deal with such cases properly we can get a buggy application that shows
results for *computer* even if the search box reads *car*.
Now that we identified the problems that need to be solved, let's make a couple of trivial changes to our code to fix them in a *functional reactive* way. Here is the
entire example with all changes.
There are no changes for our `WikipediaService` so we can skip that one. We'll go over each change to briefly describe what
it does.
+makeExample('server-communication/ts/app/wiki/wiki-form.component.ts', null, 'app/wiki/wiki-form.component.ts')
:marked
The first thing we need to do to unveil the full magic of a observable-based solution is to get an `Observable<string>` for our `<input>` control.
The best way to do that is to change `<input #term (keyup)="search(term.value)"/>` into `<input [ng-form-control]="inputs"/>` and to create an `inputs` `Control` accordingly.
+makeExample('server-communication/ts/app/wiki/wiki-form.component.ts', 'control')
:marked
We now have an `Observable<string>` at `this.inputs.valueChanges`. We can simply use the `debounceTime(ms)` and `distinctUntilChanged()` operators to fix the first two problems. We basically get a new `Observable<string>` that emits
new values exactly the way we want them to be emitted.
+makeExample('server-communication/ts/app/wiki/wiki-form.component.ts', 'distinctdebounce')(format=".")
:marked
With the previous change we tamed the input but we still need to deal with the out-of-order cases. At this point we have an `Observable<string>` and a
`search(term)` method on the `WikipediaService` that returns an `Observable<Array<string>>`. What we want is an `Observable<Array<string>>` that
carries the results of the *last* term that was emitted from the `Observable<string>`.
If we would just map our current `Observable<string>` like `.map(term => wikipediaService.search(term))` we would transform it into an `Observable<Observable<Array<string>>`
which isn't quite what we want. Enter [`switchMap`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md) to the rescue. Basically `switchMap` flattens from an `Observable<Observable<T>` into an `Observable<T>` by
emitting values only from the most recent `Observable<T>` that was produced from the outer `Observable`.
This may sound a lot like black magic for people unfamiliar with Observables but as soon as the coin sinks in it's starting to make a whole world of difficult programming tasks appear much simpler.
<a id="in-mem-web-api"></a>
.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
Our sample saves data. We can't save to a JSON file. We'll need a server ... or a server simulation.
This sample uses an in-memory web api simulator that we first installed with `npm`:
code-example.
npm install a2-in-memory-web-api --save
:marked
and then loaded it in our html below the angular script:
+makeExample('server-communication/ts/index.html', 'in-mem-web-api', 'index.html')(format=".")
:marked
The in-memory web api gets its data from a class with a `createDb()` method that returns
a "database" object whose keys are collection names ("heroes")
and whose values are arrays of objects in those collections.
Here's the class we created for this sample by copy-and-pasting the JSON data:
+makeExample('server-communication/ts/app/hero-data.ts', null, 'app/hero-data.ts')(format=".")
:marked
Finally, we tell Angular to direct its http requests to the in-memory web api service rather
than to a remote server.
This redirection is easy in Angular because `http` delegates the client/server communication tasks
to a helper service called the `XHRBackend`.
To enable our server simulation, we replace the `XHRBackend` service with
the in-memory web api *backend* using standard Angular provider registration
in the `TohComponent`. We supply the hero data to the in-memory web api in the same way at the same time.
Here are the pertinent details, excerpted from `TohComponent`, starting with the imports:
+makeExample('server-communication/ts/app/toh/toh.component.ts', 'in-mem-web-api-imports', 'toh.component.ts (web api imports)')(format=".")
:marked
Then add these two provider definitions to the component's `providers` array in metadata:
+makeExample('server-communication/ts/app/toh/toh.component.ts', 'in-mem-web-api-providers', 'toh.component.ts (web api providers')(format=".")
:marked
See the full source code in the [live example](/resources/live-examples/server-communication/ts/plnkr.html).