docs(http-client): edit copy and true TOC to doc shape (#3360)

This commit is contained in:
Kapunahele Wong 2017-03-09 15:54:14 -05:00 committed by Jules Kremer
parent a8ad96eb16
commit 4dbd920d46
2 changed files with 117 additions and 78 deletions

View File

@ -57,7 +57,7 @@ export class HeroService {
// #docregion error-handling
private handleError (error: Response | any) {
// In a real world app, we might use a remote logging infrastructure
// In a real world app, you might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';

View File

@ -17,27 +17,48 @@ block includes
[Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
The !{_Angular_http_library} simplifies application programming with the **XHR** and **JSONP** APIs.
This page covers:
- [The Tour of Heroes *HTTP* client demo](#http-client).
- [Fetch data with http.get](#fetch-data).
<li if-docs="ts"> [RxJS library](#rxjs).</li>
<li if-docs="ts"> [Enable RxJS operators](#enable-rxjs-operators).</li>
- [Process the response object](#extract-data).
- [Always handle errors](#error-handling).
- [Send data to the server](#update).
<li if-docs="ts"> [Fall back to promises](#promises).</li>
- [Cross-Origin Requests: Wikipedia example](#cors).
<ul if-docs="ts">
<li> [Search parameters](#search-parameters).</li>
<li> [More fun with observables](#more-observables).</li>
# Contents
* [Demos](#demos)
* [Providing HTTP Services](#http-providers)
* [The Tour of Heroes *HTTP* client demo](#http-client)
- [The `HeroListComponent` class](#HeroListComponent)
* [Fetch data with `http.get()`](#fetch-data)
<li if-docs="ts"> [RxJS library](#rxjs-library)
<ul>
<li> [Enable RxJS operators](#enable-rxjs-operators)</li>
</ul>
- [Guarding against Cross-Site Request Forgery](#xsrf).
- [Override default request headers (and other request options)](#override-default-request-options).
- [Appendix: Tour of Heroes _in-memory web api_](#in-mem-web-api).
</li>
* [Process the response object](#extract-data)
- [Parse to `JSON`](#parse-to-json)
- [Do not return the response object](#no-return-response-object)
- [Always handle errors](#error-handling)
- [`HeroListComponent` error handling](#hero-list-component)
* [Send data to the server](#update)
- [Headers](#headers)
- [JSON results](#json-results)
<ul><li if-docs="ts"> [Fall back to promises](#promises)</ul>
* [Cross-Origin Requests: Wikipedia example](#cors)
<ul if-docs="ts">
<li> [Search Wikipedia](#search-wikipedia)</li>
<li> [Search parameters](#search-parameters)</li>
<li> [The WikiComponent](#wikicomponent)</li>
</ul>
* [A wasteful app](#wasteful-app)
<li if-docs="ts"> [More fun with Observables](#more-observables)
<ul>
<li> [Create a stream of search terms](#create-stream)</li>
<li> [Listen for search terms](#listen-for-search-terms)</li>
</ul>
</li>
* [Guarding against Cross-Site Request Forgery](#xsrf)
* [Override default request headers (and other request options)](#override-default-request-options)
* [Appendix: Tour of Heroes _in-memory web api_](#in-mem-web-api)
A <live-example>live example</live-example> illustrates these topics.
a#demos
.l-main-section
:marked
# Demos
@ -49,7 +70,7 @@ block demos-list
- [The Tour of Heroes *HTTP* client demo](#http-client).
- [Fall back to !{_Promise}s](#promises).
- [Cross-Origin Requests: Wikipedia example](#cors).
- [More fun with observables](#more-observables).
- [More fun with Observables](#more-observables).
:marked
The root `AppComponent` orchestrates these demos:
@ -92,7 +113,7 @@ block http-providers
.l-sub-section
:marked
The `HttpModule` is necessary for making HTTP calls.
Though the JsonpModule isn't necessary for plain HTTP,
Though the `JsonpModule` isn't necessary for plain HTTP,
there is a JSONP demo later in this page.
Loading its module now saves time.
.l-main-section#http-client
@ -160,7 +181,7 @@ block getheroes-and-addhero
a#HeroService
.l-main-section#fetch-data
:marked
## Fetch data with http.get
## Fetch data with _http.get()_
In many of the previous samples the app faked the interaction with the server by
returning mock heroes in a service like this one:
@ -196,20 +217,21 @@ a#HeroService
Clearly this is not a 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*.
and `map()` is one of the RxJS *operators*.
a#rxjs-library
.l-main-section
:marked
## RxJS library
<a href="http://reactivex.io/rxjs" target="_blank" title="RxJS Reactive Extensions">RxJS</a>
is a third party library, endorsed by Angular, that implements the
<a href="" target="_blank" title="Video: Rob Wormald on observables"><b>asynchronous observable</b></a> pattern.
<a href="https://www.youtube.com/watch?v=VLGCCpOWFFw" target="_blank" title="Video: Rob Wormald on Observables"><b>asynchronous Observable</b></a> pattern.
All of the Developer Guide samples have installed the RxJS npm package
because observables are used widely in Angular applications.
because Observables are used widely in Angular applications.
_This_ app needs it when working with the HTTP client.
But you must take a critical extra step to make RxJS observables usable:
you must import the RxJS operators individually.
But you must take a critical extra step to make RxJS Observables usable:
_you must import the RxJS operators individually_.
### Enable RxJS operators
The RxJS library is large.
@ -217,19 +239,21 @@ a#HeroService
You should include only necessary features.
Each code file should add the operators it needs by importing from an RxJS library.
The `getHeroes` method needs the `map` and `catch` operators so it imports them like this.
The `getHeroes()` method needs the `map()` and `catch()` operators so it imports them like this.
+makeExample('server-communication/ts/src/app/toh/hero.service.ts', 'rxjs-imports', 'src/app/app.component.ts (import rxjs)')(format=".")
.l-main-section
a#extract-data
:marked
## Process the response object
Remember that the `getHeroes()` method used an `!{_priv}extractData` helper method to map the `!{_priv}http.get` response object to heroes:
Remember that the `getHeroes()` method used an `!{_priv}extractData()` helper method to map the `!{_priv}http.get` response object to heroes:
+makeExample('server-communication/ts/src/app/toh/hero.service.ts', 'extract-data', 'src/app/toh/hero.service.ts (excerpt)')(format=".")
:marked
The `response` object doesn't hold the data in a form the app can use directly.
You must parse the response data into a JSON object.
a#parse-to-json
:marked
### Parse to JSON
block parse-json
:marked
@ -256,6 +280,8 @@ block parse-json
:marked
Make no assumptions about the server API.
Not all servers return an object with a `data` property.
a#no-return-response-object
:marked
### Do not return the response object
The `getHeroes()` method _could_ have returned the HTTP response but this wouldn't
@ -268,9 +294,9 @@ block parse-json
.callout.is-important
header HTTP GET is delayed
:marked
The `!{_priv}http.get` does **not send the request just yet.** This observable is
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 that the request won't go out until something *subscribes* to the observable.
which means that the request won't go out until something *subscribes* to the Observable.
That *something* is the [HeroListComponent](#subscribe).
a#error-handling
@ -288,9 +314,9 @@ a#error-handling
block error-handling
:marked
The `catch` operator passes the error object from `http` to the `handleError` method.
The `catch()` operator passes the error object from `http` to the `handleError()` method.
The `handleError` method transforms the error into a developer-friendly message,
logs it to the console, and returns the message in a new, failed observable via `Observable.throw`.
logs it to the console, and returns the message in a new, failed Observable via `Observable.throw`.
a#subscribe
a#hero-list-component
@ -325,7 +351,7 @@ block hlc-error-handling
:marked
To implement it, you must know the server's API for creating heroes.
[This sample's data server](#server) follows typical REST guidelines.
[This sample's data server](#in-mem-web-api) follows typical REST guidelines.
It expects a [`POST`](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5) request
at the same endpoint as `GET` heroes.
It expects the new hero data to arrive in the body of the request,
@ -339,12 +365,13 @@ code-example(format="." language="javascript").
of the new hero including its generated id. The hero arrives tucked inside a response object
with its own `data` property.
Now that you know how the API works, implement `addHero()`as follows:
Now that you know how the API works, implement `addHero()` as follows:
+ifDocsFor('ts')
+makeExample('server-communication/ts/src/app/toh/hero.service.ts', 'import-request-options', 'src/app/toh/hero.service.ts (additional imports)')(format=".")
+makeExample('server-communication/ts/src/app/toh/hero.service.ts', 'addhero', 'src/app/toh/hero.service.ts (addHero)')(format=".")
a#headers
:marked
### Headers
@ -356,8 +383,9 @@ code-example(format="." language="javascript").
object is a new instance of `RequestOptions`, a class that allows you to specify
certain settings when instantiating a request. In this way, [headers](../api/http/index/Headers-class.html) is one of the [RequestOptions](../api/http/index/RequestOptions-class.html).
In the `return` statement, `options` is the *third* argument of the `post` method, as shown above.
In the `return` statement, `options` is the *third* argument of the `post()` method, as shown above.
a#json-results
:marked
### JSON results
@ -366,7 +394,7 @@ code-example(format="." language="javascript").
block hero-list-comp-add-hero
:marked
Back in the `HeroListComponent`, *its* `addHero()` method subscribes to the observable returned by the *service's* `addHero()` method.
Back in the `HeroListComponent`, *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/src/app/toh/hero-list.component.ts', 'addHero', 'src/app/toh/hero-list.component.ts (addHero)')(format=".")
@ -375,49 +403,54 @@ block hero-list-comp-add-hero
:marked
Although the Angular `http` client API returns an `Observable<Response>` you can turn it into a
[`Promise<Response>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
It's easy to do, and in simple cases, a promise-based version looks much
like the observable-based version.
It's easy to do, and in simple cases, a Promise-based version looks much
like the Observable-based version.
.l-sub-section
:marked
While promises may be more familiar, observables have many advantages.
While Promises may be more familiar, Observables have many advantages.
:marked
Here is a comparison of the `HeroService` using promises versus observables,
Here is a comparison of the `HeroService` using Promises versus Observables,
highlighting just the parts that are different.
+makeTabs(
'server-communication/ts/src/app/toh/hero.service.promise.ts,server-communication/ts/src/app/toh/hero.service.ts',
'methods, methods',
'src/app/toh/hero.service.promise.ts (promise-based), src/app/toh/hero.service.ts (observable-based)')
:marked
You can follow the promise `then(this.extractData).catch(this.handleError)` pattern as in
You can follow the Promise `then(this.extractData).catch(this.handleError)` pattern as in
this example.
Alternatively, you can call `toPromise(success, fail)`. The observable's `map` callback moves to the first *success* parameter and its `catch` callback to the second *fail* parameter in this pattern: `.toPromise(this.extractData, this.handleError)`.
Alternatively, you can call `toPromise(success, fail)`. The Observable's `map` callback moves to the
first *success* parameter and its `catch` callback to the second *fail* parameter
in this pattern: `.toPromise(this.extractData, this.handleError)`.
The `errorHandler` forwards an error message as a failed promise instead of a failed `observable`.
The `errorHandler` forwards an error message as a failed `Promise` instead of a failed `Observable`.
The diagnostic *log to console* is just one more `then` in the promise chain.
The diagnostic *log to console* is just one more `then()` in the Promise chain.
You have to adjust the calling component to expect a `Promise` instead of an `observable`:
You have to adjust the calling component to expect a `Promise` instead of an `Observable`:
+makeTabs(
'server-communication/ts/src/app/toh/hero-list.component.promise.ts, server-communication/ts/src/app/toh/hero-list.component.ts',
'methods, methods',
'src/app/toh/hero-list.component.promise.ts (promise-based), src/app/toh/hero-list.component.ts (observable-based)')
:marked
The only obvious difference is that you call `then` on the returned promise instead of `subscribe`.
The only obvious difference is that you call `then()` on the returned Promise instead of `subscribe`.
Both methods take 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. You can keep chaining more `then` and `catch` calls, getting a new promise each time.
The Promise-based `then()` returns another Promise. You 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. You can't call `map` on it or call `subscribe` again.
The `subscribe()` method returns a `Subscription`. A `Subscription` is not another `Observable`.
It's the end of the line for Observables. You can't call `map()` on it or call `subscribe()` again.
The `Subscription` object has a different purpose, signified by its primary method, `unsubscribe`.
To understand the implications and consequences of subscriptions, watch [Ben Lesh's talk on observables](https://www.youtube.com/watch?v=3LKMwkuK0ZE) or his video course on [egghead.io](https://egghead.io/lessons/rxjs-rxjs-observables-vs-promises).
To understand the implications and consequences of subscriptions,
watch [Ben Lesh's talk on Observables](https://www.youtube.com/watch?v=3LKMwkuK0ZE)
or his video course on [egghead.io](https://egghead.io/lessons/rxjs-rxjs-observables-vs-promises).
h2#cors Cross-Origin Requests: Wikipedia example
:marked
@ -432,7 +465,7 @@ h2#cors Cross-Origin Requests: Wikipedia example
: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, you'll enable them in the [request headers](#headers).
If the server requires user credentials, enable them in the [request headers](#headers).
:marked
Some servers do not support CORS but do support an older, read-only alternative called [JSONP](https://en.wikipedia.org/wiki/JSONP).
@ -440,8 +473,9 @@ h2#cors Cross-Origin Requests: Wikipedia example
.l-sub-section
:marked
This [Stack Overflow answer](http://stackoverflow.com/questions/2067472/what-is-jsonp-all-about/2067584#2067584) covers many details of JSONP.
a#search-wikipedia
:marked
### Search wikipedia
### Search Wikipedia
Here is a simple search that shows suggestions from Wikipedia as the user
types in a text box:
@ -453,13 +487,13 @@ block wikipedia-jsonp+
:marked
Wikipedia offers a modern `CORS` API and a legacy `JSONP` search API. This example uses the latter.
The Angular `Jsonp` service both extends the `!{_Http}` service for JSONP and restricts you to `GET` requests.
All other HTTP methods throw an error because JSONP is a read-only facility.
All other HTTP methods throw an error because `JSONP` is a read-only facility.
As always, wrap the interaction with an Angular data access client service inside a dedicated service, here called `WikipediaService`.
+makeExample('server-communication/ts/src/app/wiki/wikipedia.service.ts',null,'src/app/wiki/wikipedia.service.ts')
:marked
The constructor expects Angular to inject its `jsonp` service, which
The constructor expects Angular to inject its `Jsonp` service, which
is available because `JsonpModule` is in the root `@NgModule` `imports` array
in `app.module.ts`.
@ -486,7 +520,8 @@ block wikipedia-jsonp+
This time you call `jsonp` with *two* arguments: the `wikiUrl` and an options object whose `search` property is the `params` object.
+makeExample('server-communication/ts/src/app/wiki/wikipedia.service.ts','call-jsonp','src/app/wiki/wikipedia.service.ts (call jsonp)')(format=".")
:marked
`Jsonp` flattens the `params` object into the same query string you saw earlier, putting the request on the wire.
`Jsonp` flattens the `params` object into the same query string you saw earlier, sending the request
to the server.
<a id="wikicomponent"></a>
:marked
@ -500,26 +535,27 @@ block wikipedia-jsonp+
and calls a `search(term)` method after each `keyup` event.
The component's `search(term)` method delegates to the `WikipediaService`, which returns an
observable array of string results (`Observable<string[]>`).
Instead of subscribing to the observable inside the component, as in the `HeroListComponent`,
the app forwards the observable result to the template (via `items`) where the `async` pipe
Observable !{_array} of string results (`Observable<string[]>`).
Instead of subscribing to the Observable inside the component, as in the `HeroListComponent`,
the app forwards the Observable result to the template (via `items`) where the `async` pipe
in the `ngFor` handles the subscription. Read more about [async pipes](pipes.html#async-pipe)
in the [Pipes](pipes.html) page.
.l-sub-section
:marked
The [async pipe](pipes.html#async-pipe) is a good choice in read-only components where the component has no need to interact with the data.
The [async pipe](pipes.html#async-pipe) is a good choice in read-only components
where the component has no need to interact with the data.
`HeroListComponent` can't use the pipe because `addHero()` pushes newly created heroes into the list.
a#wasteful-app
:marked
## A wasteful app
The wikipedia search makes too many calls to the server.
It is inefficient, and potentially expensive on mobile devices with limited data plans.
The Wikipedia search makes too many calls to the server.
It is inefficient and potentially expensive on mobile devices with limited data plans.
### 1. Wait for the user to stop typing
Presently, the code calls the server after every keystroke.
It should only make requests when the user *stops typing* .
It should only make requests when the user *stops typing*.
Here's how it will work after refactoring:
figure.image-display
img(src='/resources/images/devguide/server-communication/wiki-2.gif' alt="Wikipedia search app (v.2)" width="250")
@ -544,13 +580,13 @@ block wikipedia-jsonp+
no matter which response arrives first.
<a id="more-observables"></a>
## More fun with observables
## More fun with Observables
You could make changes to the `WikipediaService`, but for a better
user experience, create a copy of the `WikiComponent` instead and make it smarter,
with the help of some nifty observable operators.
with the help of some nifty Observable operators.
Here's the `WikiSmartComponent`, shown next to the original `WikiComponent`
Here's the `WikiSmartComponent`, shown next to the original `WikiComponent`:
+makeTabs(
`server-communication/ts/src/app/wiki/wiki-smart.component.ts,
@ -563,21 +599,24 @@ block wikipedia-jsonp+
While the templates are virtually identical,
there's a lot more RxJS in the "smart" version,
starting with `debounceTime`, `distinctUntilChanged`, and `switchMap` operators,
imported as [described above](#rxjs).
imported as [described above](#rxjs-library).
a#create-stream
:marked
### Create a stream of search terms
The `WikiComponent` passes a new search term directly to the `WikipediaService` after every keystroke.
The `WikiSmartComponent` class turns the user's keystrokes into an observable _stream of search terms_
The `WikiSmartComponent` class turns the user's keystrokes into an Observable _stream of search terms_
with the help of a `Subject`, which you import from RxJS:
+makeExample('server-communication/ts/src/app/wiki/wiki-smart.component.ts', 'import-subject')(format='.')
:marked
The component creates a `searchTermStream` as a `Subject` of type `string`.
The `search` method adds each new search box value to that stream via the subject's `next` method.
The `search()` method adds each new search box value to that stream via the subject's `next()` method.
+makeExample('server-communication/ts/src/app/wiki/wiki-smart.component.ts', 'subject')(format='.')
a#listen-for-search-terms
:marked
### Listen for search terms
@ -595,11 +634,11 @@ block wikipedia-jsonp+
calls the `WikipediaService` with a fresh, debounced search term and coordinates the stream(s) of service response.
The role of `switchMap` is particularly important.
The `WikipediaService` returns a separate observable of string arrays (`Observable<string[]>`) for each search request.
The `WikipediaService` returns a separate Observable of string arrays (`Observable<string[]>`) for each search request.
The user could issue multiple requests before a slow server has had time to reply,
which means a backlog of response observables could arrive at the client, at any moment, in any order.
which means a backlog of response Observables could arrive at the client, at any moment, in any order.
The `switchMap` returns its own observable that _combines_ all `WikipediaService` response observables,
The `switchMap` returns its own Observable that _combines_ all `WikipediaService` response Observables,
re-arranges them in their original request order,
and delivers to subscribers only the most recent search results.
@ -609,7 +648,7 @@ a#xsrf
## Guarding against Cross-Site Request Forgery
In a cross-site request forgery (CSRF or XSRF), an attacker tricks the user into visiting
a different web page with malignant code that secretly sends a malicious request to your application's web server,
a different web page with malignant code that secretly sends a malicious request to your application's web server.
The server and client application must work together to thwart this attack.
Angular's `Http` client does its part by applying a default `CookieXSRFStrategy` automatically to all requests.
@ -631,7 +670,7 @@ a#override-default-request-options
before the request is processed.
The `HttpModule` provides these default options via the `RequestOptions` token.
You can override these defaults to suit your application needs.
You can override these defaults to suit your application needs
by creating a custom sub-class of `RequestOptions`
that sets the default options for the application.
@ -646,7 +685,7 @@ a#override-default-request-options
:marked
Remember to include this provider during setup when unit testing the app's HTTP services.
:marked
After this change, the `header` option setting in `HeroService.addHero` is no longer necessary,
After this change, the `header` option setting in `HeroService.addHero()` is no longer necessary,
+makeExample('server-communication/ts/src/app/toh/hero.service.ts', 'addhero', 'src/app/toh/hero.service.ts (addHero)')(format=".")
:marked
@ -709,7 +748,7 @@ block redirect-to-web-api
:marked
Finally, redirect client HTTP requests to the in-memory web API by
adding the `InMemoryWebApiModule` to the `AppModule.imports` list.
At the same time, call its `forRoot` configuration method with the `HeroData` class.
At the same time, call its `forRoot()` configuration method with the `HeroData` class.
+makeExample('server-communication/ts/src/app/app.module.ts', 'in-mem-web-api', 'src/app/app.module.ts')(format=".")
:marked
### How it works
@ -722,7 +761,7 @@ block redirect-to-web-api
At the same time, the `forRoot` method initializes the in-memory web API with the *seed data* from the mock hero dataset.
.l-sub-section
:marked
The `forRoot` method name is a strong reminder that you should only call the `InMemoryWebApiModule` _once_,
The `forRoot()` method name is a strong reminder that you should only call the `InMemoryWebApiModule` _once_,
while setting the metadata for the root `AppModule`. Don't call it again.
:marked
Here is the final, revised version of <span ngio-ex>src/app/app.module.ts</span>, demonstrating these steps.