docs(server-communication): discuss promises; refine prose

closes #767
This commit is contained in:
Christoph Burgdorf 2016-01-28 10:22:59 +01:00 committed by Ward Bell
parent a7826ee44c
commit 8a5bcb4da7
6 changed files with 139 additions and 66 deletions

View File

@ -18,34 +18,37 @@ import {HeroService} from './hero.service.1';
<button (click)="addHero(newHero.value); newHero.value=''">
Add Hero
</button>
<div class="error" *ngIf="errorMessage">{{errorMessage}}</div>
`,
// #enddocregion template
styles: ['.error {color:red;}']
})
// #docregion component
export class HeroListComponent implements OnInit {
constructor (private _heroService: HeroService) {}
errorMessage: string;
heroes:Hero[];
// #docregion ngOnInit
ngOnInit() {
ngOnInit() { this.getHeroes(); }
// #docregion methods
getHeroes() {
this._heroService.getHeroes()
.then(
heroes => this.heroes = heroes,
error => alert(`Server error. Try again later`));
error => this.errorMessage = <any>error);
}
// #enddocregion ngOnInit
// #docregion addHero
addHero (name: string) {
if (!name) {return;}
this._heroService.addHero(name)
.then(
hero => this.heroes.push(hero),
error => alert(error));
error => this.errorMessage = <any>error);
}
// #enddocregion addHero
// #enddocregion methods
}
// #enddocregion component

View File

@ -17,23 +17,29 @@ import {HeroService} from './hero.service';
<button (click)="addHero(newHero.value); newHero.value=''">
Add Hero
</button>
<div class="error" *ngIf="errorMessage">{{errorMessage}}</div>
`,
styles: ['.error {color:red;}']
})
// #docregion component
export class HeroListComponent implements OnInit {
constructor (private _heroService: HeroService) {}
errorMessage: string;
heroes:Hero[];
// #docregion ngOnInit
ngOnInit() {
ngOnInit() { this.getHeroes(); }
// #docregion methods
// #docregion getHeroes
getHeroes() {
this._heroService.getHeroes()
.subscribe(
heroes => this.heroes = heroes,
error => alert(`Server error. Try again later`));
error => this.errorMessage = <any>error);
}
// #enddocregion ngOnInit
// #enddocregion getHeroes
// #docregion addHero
addHero (name: string) {
@ -41,9 +47,10 @@ export class HeroListComponent implements OnInit {
this._heroService.addHero(name)
.subscribe(
hero => this.heroes.push(hero),
error => alert(error));
error => this.errorMessage = <any>error);
}
// #enddocregion addHero
// #enddocregion methods
}
// #enddocregion component

View File

@ -2,36 +2,36 @@
// #docplaster
// #docregion
// #docregion v1
import {Injectable} from 'angular2/core';
import {Http} from 'angular2/http';
import {Hero} from './hero';
import {Injectable} from 'angular2/core';
import {Http, Response} from 'angular2/http';
import {Hero} from './hero';
@Injectable()
export class HeroService {
constructor (private http: Http) {}
private _heroesUrl = 'app/heroes.json';
private _heroesUrl = 'app/heroes';
// #docregion methods
getHeroes () {
// TODO: Error handling
// #docregion http-get
return this.http.get(this._heroesUrl)
.toPromise()
.then(res => <Hero[]> res.json().data);
// #enddocregion http-get
.then(res => <Hero[]> res.json().data, this.handleError);
}
// #enddocregion v1
// #docregion addhero
addHero (name: string) : Promise<Hero> {
// TODO: Error handling
return this.http.post(this._heroesUrl, JSON.stringify({ name }))
.toPromise()
.then(res => <Hero> res.json().data);
.then(res => <Hero> res.json().data)
.catch(this.handleError);
}
// #enddocregion addhero
// #docregion v1
private handleError (error: any) {
// in a real world app, we may send the server to some remote logging infrastructure
// instead of just logging it to the console
console.error(error);
return Promise.reject(error.message || error.json().error || 'Server error');
}
// #enddocregion methods
}
// #enddocregion v1
// #enddocregion

View File

@ -2,45 +2,48 @@
// #docregion
// #docregion v1
import {Injectable} from 'angular2/core';
import {Http} from 'angular2/http';
import {Hero} from './hero';
import {Observable} from 'rxjs/Observable';
import {Injectable} from 'angular2/core';
import {Http, Response} from 'angular2/http';
import {Hero} from './hero';
import {Observable} from 'rxjs/Observable';
@Injectable()
export class HeroService {
constructor (private http: Http) {}
private _heroesUrl = 'app/heroes.json';
// #docregion endpoint
private _heroesUrl = 'app/heroes';
// #enddocregion endpoint
// #docregion methods
// #docregion error-handling
getHeroes () {
// #docregion http-get
return this.http.get(this._heroesUrl)
.map(res => <Hero[]> res.json().data)
.catch(this.logAndPassOn);
.catch(this.handleError);
// #enddocregion http-get
}
// #docregion logAndPassOn
private logAndPassOn (error: Error) {
// in a real world app, we may send the server to some remote logging infrastructure
// instead of just logging it to the console
console.error(error);
return Observable.throw(error);
}
// #enddocregion logAndPassOn
// #enddocregion error-handling
// #enddocregion v1
// #enddocregion error-handling
// #enddocregion v1
// #docregion addhero
addHero (name: string) : Observable<Hero> {
return this.http.post(this._heroesUrl, JSON.stringify({ name }))
.map(res => <Hero> res.json().data)
.catch(this.logAndPassOn)
.catch(this.handleError)
}
// #enddocregion addhero
// #docregion v1
// #docregion v1
// #docregion error-handling
private handleError (error: Response) {
// in a real world app, we may send the server to some remote logging infrastructure
// instead of just logging it to the console
console.error(error);
return Observable.throw(error.json().error || 'Server error');
}
// #enddocregion error-handling
// #enddocregion methods
}
// #enddocregion v1
// #enddocregion

View File

@ -5,5 +5,5 @@
"!**/*.js",
"!**/*.[1].*"
],
"tags": ["http"]
"tags": ["http", "jsonp"]
}

View File

@ -40,12 +40,12 @@ figure.image-display
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`,
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,
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
@ -63,6 +63,8 @@ figure.image-display
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.
@ -83,7 +85,11 @@ figure.image-display
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
Now that the component is square away, we can turn to development of the backend data source
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.
### Fetching data
@ -182,26 +188,35 @@ figure.image-display
### Always handle errors
The eagle-eyed reader may have spotted that we used the `catch` operator in conjunction with the `logAndPassOn` method that we
defined in our service. We haven't discussed so far how that actually works. Generally speaking, whenever we deal with I/O we have to account
for the cases where something goes wrong. It may just be an unreachable server or something more subtile.
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 could just ignore any errors in our `HeroService` and rely on the component to handle such cases. But it's often better to
handle errors on both levels: Service and Component. In our case that may be as simple as logging the errors to the console at the service level
and blowing up an alert box in the user's face at the component level.
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.
The `catch` operator lets us do exactly that. This operator reacts to the error case of an Observable. It takes a function that can handle the error
and then return a new Observable to continue with. Because we like to keep things simple here, we just defined a function `logAndPassOn` that calls `console.error(error)` and returns a
new Observable that re-emits the error with `Observable.throw(error)`.
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 call `heroService.get`,
we pass a second parameter to the `subscribe` function to handle the error case at this level too.
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', 'ngOnInit', 'app/toh/hero-list.component.ts (ngOnInit)')
+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)')
:marked
### Sending data to the server
@ -242,6 +257,51 @@ code-example(format="." language="javascript").
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=".")
:marked
## Falling 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.
:marked
## Communication with `JSONP`
@ -409,4 +469,4 @@ code-example.
+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).