原文链接:http://blog.rangle.io/observables-and-reactive-programming-in-angular-2/
Observables and Reactive Programming in Angular 2
Written by Tyler Borchert
<p><strong>This post was last updated 04/08/2016. Please refer to our <a href="http://go.rangle.io/angular2-ebook">Angular 2 eBook</a> for the most recent updates.</strong></p>
Angular 2 embraces elements of reactive programming, a paradigm focused on how data flows through an application. To follow the reactive approach, we use Observables to build services and components that can react to changes in data across our application. Observables aren’t exactly an Angular 2 specific feature, they’re a part of the JavaScript Reactive Extensions library (RxJS) and are set to be an included feature in the release of ES7. In this post, we’ll explore the motivation for Observables and the concepts of Reactive Programming, and how to use Observables in your application patterns.
Why Observables?
The main purpose of using Observables is to observe the behaviour of a variable. In an imperative way, a variable is only changed when its state is mutated by assigning a new or updated value. The reactive approach allows variables to develop a history of evolving change, asynchronously being updated to the values of other points of data.
Let’s take a look at how both approaches differ — consider capturing the mouse movements in a browser window. The imperative way might look something like this:
let mouse;
document.onmousemove = (e) => mouse = [e.pageX, e.pageY];
Anytime we get an event that the mouse has moved, we re-assign mouse
to the updated coordinates. The state of mouse
is discrete in that each re-assignment overrides the previous one, removing any continuous connection.
Now let’s see the reactive way:
let mouse = document.onmousemove.subscribe()
Here mouse
is only assigned once and the values it receives are stored in a stream of continuous activity. The values captured by mouse
are stored in a what amounts to a FIFO queue; they can be iterated on using standard array operations. Pretty cool stuff, unfortunately, onmousemove
is not actually an observable we can subscribe to, so this code example won’t work in current browsers — yet. ES7 is being released this year and will include built-in support for Observables - can’t wait!
In the meantime, RxJS
provides an implementation that we can use right away.
Anatomy of an Observable
Observables follow the Observer pattern, where a single piece of state (the Observable
) is watched by one or more Observers
which can react as it changes over time.
At a low level, the Observable acts as an event emitter, sending a stream of events to any subjects that have subscribed to it. The Observable object doesn’t contain any references to the subject classes itself: instead, an event bus is used to decouple publishing and subscribing concerns. What we get is an asynchronous stream of values that can be operated on using common iteration patterns – exactly the sort of thing that will help us tackle our data concerns using a reactive methodology.
hbspt.cta.load(468143, '51ccf952-d46c-4768-9ff3-c502437fa647', {});Using Observables in Angular 2 Components
The great advantage of reactive programming is the ability to connect points of data together. A common task when building any application is to determine the way your data will travel from a network request or user input to the visual UI components on the screen. This is where reactive programming shines.
In Angular 2, our components have access to pipes, which can be used to transform values via simple expressions in our templates. Pipes are very powerful tools and we’ll use them to connect component data across our application. A common way of separating components is to break the design up into dumb and smart components. A dumb component’s only job is to present data to the UI view and emit any UI events, and a smart component is concerned with what the dumb components should be displaying. Using Observables and AsyncPipe
we can set up a bridge of data to flow between our smart and dumb components. Smart components can pass data to display, and the dumb components can pass any input events that they capture from the UI.
How does this look in action? Well, let’s build out a simple Spotify search component. We’ll need a dumb component that will capture user input, and display any search results that have come in. What will our smart component do? It will use the HTTP service to get search results and apply any transformations to the data that we need.
Before we begin, to use Observables you need to import the rxjs
dependency into your Angular 2 project. Our examples here will use a hosted CDN to do that, but if you’re using webpack
or browserify
for your project then just include the rxjs
node module in the dependencies field of your package.json
and import
it into your app code. You can view the entire working example here. OK sweet, let’s get started by looking at our dumb component!
Dumb Component
import {Control, FORM_DIRECTIVES} from 'angular2/common';
import {Component, Output, Input, EventEmitter} from 'angular2/core';
import 'rxjs/Rx';
@Component({
selector: 'spotify-search',
directives: [FORM_DIRECTIVES],
template: `
<h1>Spotify Search</h1>
<input
[ngFormControl]="searchBox"
placeholder="Search Spotify artist" />
<div *ngFor="#artists of results | async">
{{ artists.name }} -- Popularity {{ artists.popularity }}
</div>
`
})
export class SpotifySearch {
@Input() results: Observable<any>;
@Output() searchEvent: EventEmitter = new EventEmitter();
private searchBox:Control = new Control();
constructor() {
this.searchBox
.valueChanges
.debounceTime(200)
.subscribe((event) => this.searchEvent.emit(event));
}
}
Here, in all its glory, is our dumb component - responsible for displaying UI elements and capturing UI events. We have a list of artists to display, which comes from an Observable named results
that is decorated as an Input, meaning it will come from the parent smart component. These results are coming in as an array of artist objects, so we can iterate through each one and display some properties using the *ngFor
directive. You’ll notice that in our expression we utilize the async
pipe, which will subscribe to any values that are being emitted from results
automatically. The pipe maintains a subscription to results
and keeps delivering values as they arrive. What’s great with this pattern is the decoupling of data concerns from UI concerns. By providing an Observable to interface with this dumb component, our data can come from a service, a redux store, or any other abstraction we want to model our data with. As long as we push our data through the Observable stream, our dumb component will display it. Pretty sweet huh?
Our dumb component also has an input field, which makes this component responsible for capturing events generated by the user whenever they change the value of the input field. Since our smart component is the one that fetches data and transforms it, we need to let it know that some new search criteria has been entered. So, we create an EventEmitter
and decorate it as an Output coming from this component. An EventEmitter
uses Observables as its underlying mechanism but uses an Angular 2 specific adapter. This isn’t really much of a concern for us, the EventEmitter
has a subscribe
and emit
function used to observe and emit data respectively. We also use debounceTime
to debounce the volume of events that will be fired whenever a user types something into the input field. This is good practice - we don’t want our application to slow down if the user types away rapidly in our input field.
With the dumb stuff out of the way, let ‘s take a look at our smart component.
Smart Component
import {Http} from 'angular2/http';
import {Component} from 'angular2/core';
import {Input, Output, EventEmitter} from 'angular2/core';
import 'rxjs/Rx';
import {Observable} from 'rxjs/Observable';
import {SpotifySearch} from './services/Search';
@Component({
selector: 'app-root',
directives: [SpotifySearch],
template: `
<spotify-search
(searchEvent)="onSearch($event)"
[results]="data">
</spotify-search>
`
})
export class AppComponent {
private data: Observable;
private dataObserver: Observer;
constructor(private http: Http) {
this.data = new Observable(observer => this.dataObserver = observer);
}
onSearch(event) {
this.http.get(
'https://api.spotify.com/v1/search?q=' + event + '&type=artist'
).map((response) => {
var artists = response.json();
return artists.artists.items;
}).subscribe(result => {
this.dataObserver.next(result);
}, error => console.log('Could not load artists'));
}
}
OK, so our smart component is going to control what our spotify-search
component is going to display, as well as handle any new search criteria being emitted from spotify-search
. We’ll fetch data directly in this component for the sake of simplicity, but it would be best to abstract any calls to the Spotify API in a re-usable and injectable service. Better yet – using redux to keep our data in a single source of truth store would be great too, but that’s a different article for a different day. Let’s focus on how to direct our data concerns in this smart component down to the view concerns in our dumb component. To do that, we create a new variable, data
and assign it a new Observable and bind it to the results
input field in spotify-search
. This will ensure that any new values we emit to this Observable will show up in spotify-search
. We also create an Observer, assigned to dataObserver
, and set it to be the Observer used in our data
Observable. Why do we do this? Well, the dataObserver
is the object responsible for generating the values that will be emitted through the Observable. We need to have an explicit reference to it in order to push through any data we get from our HTTP requests.
Now that we have the ability to send data to our dumb component, we need to know when to actually search for data. This happens with onSearch
, which is bound to the searchEvent
output property in spotify-search
. When an event is emitted, onSearch
gets called with the value emitted by the EventEmitter Observable stream (in this case, the value of the search input). With this search criterion, we use http.get
to query the API for relevant search results, which also uses an Observable to subscribe to the incoming network response. In getting our response, we use map
to transform the contents of our data. You may have used map
before; it creates a new array with the results of calling a provided function on every element in that array. Our HTTP request only returns a single value, so our Observable stream will only have one element that map
will iterate over. We transform this item into an object suitable for consumption by converting the response
object into a JSON object, and returning the artists.items
property, which contains an array of the artist objects we want to be displayed. Finally, we push this data through our data
Observable stream by using the dataObserver
’s next()
method. Ta-da! So there you have it! Our smart and dumb components are now connected through the power of Observables, which allows our data to flow from the areas of our application concerned with data, to the one concerned with UI views. It’s a beautiful thing.
Conclusion
So what have we learned? Reactive programming offers an interesting approach to managing the way in which data evolves over time, and how it can be consumed across your application. We implemented some of these ideas using the smart/dumb component pattern in Angular 2 and got a pretty nifty Spotify search component working. Interesting stuff! Now get out there build something with these new fancy Observables!
More Angular 2 Resources
Rangle’s Angular 2 Resource Page has a tonne of great content to help you dive into all things Angular 2. Check it out for our other related blog posts, webinar recordings, and more.
Upcoming Webinar: Intro to Angular 2 Components
2-day Training with our experts, available in person, or online. Dive into Angular 2’s component model and start thinking differently about your application architecture.
View and download Rangle’s Angular 2 Training Book. Our extensive Angular 2 course book, created in-house by Rangle’s team of JavaScript experts, covers fundamental topics including how to get started with the Angular 2 toolchain.
Rangle’s Angular 2 Newsletter
Sign up for our dedicated Angular 2.0 newsletter, to gain insights on best practices and how to strategize your shift: