Taking advantage of Observables in Angular 2
2016-04-13 09:55
615 查看
Some people seem to be confused why Angular 2 seems to favor the Observable abstraction over the Promise abstraction when it comes to dealing with async behavior.
There are pretty good resources about the difference between Observables and Promises already out there. I especially like to highlight this free 7
minutes video by Ben Lesh on egghead.io.
Technically there are a couple of obvious differences like the disposability and lazyness of Observables. In this article we like to focus on some practical advantages that Observables introduce for server communication.
Consider you are building a search input mask that should instantly show you results as you type.
If you’ve ever build such a thing before you are probably aware of the challenges that come with that 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 endpoint more often than needed. Basically we only want to hit it once the user has stopped
typing instead of with every keystroke.
2. Don’t hit the search endpoint with the same query params for subsequent requests
Consider you type foo, stop, type another o, followed by an immediate backspace and rest 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. Deal 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. Now we have two requests in-flight. Unfortunately the request that carries the results for computer comes back after the request that carries the results for car. This may happen because they are served
by different servers. If we don’t deal with such cases properly we may end up showing results for computer whereas the search box reads car.
We will use the free and open wikipedia API to write a little demo.
For simplicity our demo will simply consist of two files:
In a real world scenario we would most likely split things further up though.
Let’s start with a Promise-based implementation that doesn’t handle any of the described edge cases.
This is what our
here.
Basically we are injecting the
against the wikipedia API with a given search term. Notice that we call
a
end up with a
So far so good, let’s take a look at the
Not much of a surprise here either. We inject our
to the template. The template simply binds to
Angular 2’s awesome template ref feature.
We unwrap the result of the
and expose it as a simple Array of strings to the template so that we can have
You can play with the demo and fiddle with the code through this plnkr.
Unfortunately this implementation doesn’t address any of the described edge cases that we would like to deal with. Let’s refactor our code to make it match the expected behavior.
Taming the user input
Let’s change our code to not hammer the endpoint with every keystroke but instead only send a request when the user stopped typing for 400 ms. This is where Observables really shine. The Reactive Extensions (Rx) offer a broad range of operators that let us
alter the behavior of Observables and create new Observables with the desired semantics.
To unveil such super powers we first need to get an
we use
In our component we create an instance of
expose it as a field under the name
Behind the scenes
property
taming the user input is as easy as calling
will only emit a new value when there haven’t been coming new values for 400ms.
Don’t hit me twice
As we said, it would be a waste of resources to send out another request for a search term that our app already shows the results for. Fortunately Rx simplifies many operations that it nearly feels unnecessary to mention them. All we have to do to achieve the
desired behavior is to call the
Again, we will get back an
Dealing with out-of-order responses
Dealing with out of order responses can be a tricky task. Basically we need a way to express that we aren’t interested anymore in results from previous in-flight requests as soon as we are sending out new requests. In other words: cancel out all previous request
as soon as we start a new one. As I briefly mentioned in the beginning Observables are disposable which means we can unsubscribe from them.
This is where we want to change our
of an
using
Now that our
our
But now we have two
an
our
composing the Observables.
You may be wondering what
The answer is quite simple. The
returns a value
returns a
an
So coming from an
map would take us to an
The
returns an
NOTE: That’s not entirely true, but it helps as a simplification.
That perfectly matches our case. We have an
a function that takes a
So does this solve our out-of-order response issues? Unfortunately not. So, why am I bothering you with all this in the first place? Well, now that you understood
replace it with
What?! You may be wondering if I’m kidding you but no I am not. That’s the beautify of Rx with all it’s useful operators. The
a way. Both operators automatically subscribe to the Observable that the function produces and flatten the result for us. The difference is that the
automatically unsubscribes from previous subscriptions as soon as the outer Observable emits new values.
Now that we got the semantics right, there’s one more little trick that we can use to save us some typing. Instead of manually subscribing to the Observable we can let Angular do the unwrapping for us right from within the template. All we have to do to accomplish
that is to use the
of
And voilà, here is our final version in a plnkr.
原文链接: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html
There are pretty good resources about the difference between Observables and Promises already out there. I especially like to highlight this free 7
minutes video by Ben Lesh on egghead.io.
Technically there are a couple of obvious differences like the disposability and lazyness of Observables. In this article we like to focus on some practical advantages that Observables introduce for server communication.
The scenario
Consider you are building a search input mask that should instantly show you results as you type.If you’ve ever build such a thing before you are probably aware of the challenges that come with that 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 endpoint more often than needed. Basically we only want to hit it once the user has stopped
typing instead of with every keystroke.
2. Don’t hit the search endpoint with the same query params for subsequent requests
Consider you type foo, stop, type another o, followed by an immediate backspace and rest 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. Deal 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. Now we have two requests in-flight. Unfortunately the request that carries the results for computer comes back after the request that carries the results for car. This may happen because they are served
by different servers. If we don’t deal with such cases properly we may end up showing results for computer whereas the search box reads car.
Challenge accepted
We will use the free and open wikipedia API to write a little demo.For simplicity our demo will simply consist of two files:
app.tsand
wikipedia-service.ts.
In a real world scenario we would most likely split things further up though.
Let’s start with a Promise-based implementation that doesn’t handle any of the described edge cases.
This is what our
WikipediaServicelooks like. Despite the fact that the Http/JsonP API still has some little unergonomic parts, there shouldn’t be much of surprise
here.
import {Injectable} from 'angular2/core'; import {URLSearchParams, Jsonp} from 'angular2/http'; @Injectable() export class WikipediaService { constructor(private jsonp: Jsonp) {} search (term: string) { var search = new URLSearchParams() search.set('action', 'opensearch'); search.set('search', term); search.set('format', 'json'); return this.jsonp .get('http://en.wikipedia.org/w/api.php?callback=JSONP_CALLBACK', { search }) .toPromise() .then((response) => response.json()[1]); } }
Basically we are injecting the
Jsonpservice to make a
GETrequest
against the wikipedia API with a given search term. Notice that we call
toPromisein order to get from an
Observable<Response>to
a
Promise<Response>. With a little bit of
then-chaining we eventually
end up with a
Promise<Array<string>>as the return type of our
searchmethod.
So far so good, let’s take a look at the
app.tsfile that holds our
AppComponent.
// check the plnkr for the full list of imports import {...} from '...'; @Component({ selector: 'my-app', template: ` <div> <h2>Wikipedia Search</h2> <input #term type="text" (keyup)="search(term.value)"> <ul> <li *ngFor="#item of items">{{item}}</li> </ul> </div> ` }) export class App { items: Array<string>; constructor(private wikipediaService: WikipediaService) { } search (term) { this.wikipediaService.search(term) .then(items => this.items = items); } } bootstrap(App, [WikipediaService, JSONP_PROVIDERS]) .catch(err => console.error(err));
Not much of a surprise here either. We inject our
WikipediaServiceand expose it’s functionality via a
searchmethod
to the template. The template simply binds to
keyupand calls
search(term.value)leveraging
Angular 2’s awesome template ref feature.
We unwrap the result of the
Promisethat the
searchmethod of the
WikipediaServicereturns
and expose it as a simple Array of strings to the template so that we can have
*ngForloop through it and build up a list for us.
You can play with the demo and fiddle with the code through this plnkr.
Unfortunately this implementation doesn’t address any of the described edge cases that we would like to deal with. Let’s refactor our code to make it match the expected behavior.
Taming the user input
Let’s change our code to not hammer the endpoint with every keystroke but instead only send a request when the user stopped typing for 400 ms. This is where Observables really shine. The Reactive Extensions (Rx) offer a broad range of operators that let us
alter the behavior of Observables and create new Observables with the desired semantics.
To unveil such super powers we first need to get an
Observable<string>that carries the search term that the user types in. Instead of manually binding to the
keyupevent
we use
ngFormControlfrom within our template and set it to the name
"term".
<input type="text" [ngFormControl]="term"/>
In our component we create an instance of
Controlfrom
angular2/commonand
expose it as a field under the name
termon our component.
Behind the scenes
termautomatically exposes an
Observable<string>as
property
valueChangesthat we can subscribe to. Now that we have an
Observable<string>,
taming the user input is as easy as calling
debounceTime(400)on our Observable. This will return a new
Observable<string>that
will only emit a new value when there haven’t been coming new values for 400ms.
export class App { items: Array<string>; term = new Control(); constructor(private wikipediaService: WikipediaService) { this.term.valueChanges .debounceTime(400) .subscribe(term => this.wikipediaService.search(term).then(items => this.items = items)); } }
Don’t hit me twice
As we said, it would be a waste of resources to send out another request for a search term that our app already shows the results for. Fortunately Rx simplifies many operations that it nearly feels unnecessary to mention them. All we have to do to achieve the
desired behavior is to call the
distinctUntilChangedoperator right after we called
debounceTime(400).
Again, we will get back an
Observable<string>but one that ignores values that are the same as the previous.
Dealing with out-of-order responses
Dealing with out of order responses can be a tricky task. Basically we need a way to express that we aren’t interested anymore in results from previous in-flight requests as soon as we are sending out new requests. In other words: cancel out all previous request
as soon as we start a new one. As I briefly mentioned in the beginning Observables are disposable which means we can unsubscribe from them.
This is where we want to change our
WikipediaServiceto return an
Observable<Array<string>>instead
of an
Promise<Array<string>>. That’s as easy as dropping
toPromiseand
using
mapinstead of
then.
search (term: string) { var search = new URLSearchParams() search.set('action', 'opensearch'); search.set('search', term); search.set('format', 'json'); return this.jsonp .get('http://en.wikipedia.org/w/api.php?callback=JSONP_CALLBACK', { search }) .map((response) => response.json()[1]); }
Now that our
WikipediaSericereturns an Observable instead of a Promise we simply need to replace
thenwith
subscribein
our
Appcomponent.
this.term.valueChanges .debounceTime(400) .distinctUntilChanged() .subscribe(term => this.wikipediaService.search(term).subscribe(items => this.items = items));
But now we have two
subscribecalls. This is needlessly verbose and often a sign for unidiomatic usage. The good news is, now that
searchreturns
an
Observable<Array<string>>we can simply use
flatMapto project
our
Observable<string>into the desired
Observable<Array<string>>by
composing the Observables.
this.term.valueChanges .debounceTime(400) .distinctUntilChanged() .flatMap(term => this.wikipediaService.search(term)) .subscribe(items => this.items = items);
You may be wondering what
flatMapdoes and why we can’t use
maphere.
The answer is quite simple. The
mapoperator expects a function that takes a value
Tand
returns a value
U. For instance a function that takes in a
stringand
returns a
Number. Hence when you use
mapyou get from an
Observable<T>to
an
Observable<U>. However, our
searchmethod produces an
Observable<Array>itself.
So coming from an
Observable<string>that we have right after
distinctUntilChanged,
map would take us to an
Observable<Observable<Array<string>>. That’s not quite what we want.
The
flatMapoperator on the other hand expects a function that takes a
Tand
returns an
Observable<U>and produces an
Observable<U>for us.
NOTE: That’s not entirely true, but it helps as a simplification.
That perfectly matches our case. We have an
Observable<string>, then call
flatMapwith
a function that takes a
stringand returns an
Observable<Array<string>>.
So does this solve our out-of-order response issues? Unfortunately not. So, why am I bothering you with all this in the first place? Well, now that you understood
flatMapjust
replace it with
switchMapand you are done.
What?! You may be wondering if I’m kidding you but no I am not. That’s the beautify of Rx with all it’s useful operators. The
switchMapoperator is comparable to
flatMapin
a way. Both operators automatically subscribe to the Observable that the function produces and flatten the result for us. The difference is that the
switchMapoperator
automatically unsubscribes from previous subscriptions as soon as the outer Observable emits new values.
Putting some sugar on top
Now that we got the semantics right, there’s one more little trick that we can use to save us some typing. Instead of manually subscribing to the Observable we can let Angular do the unwrapping for us right from within the template. All we have to do to accomplishthat is to use the
AsyncPipein our template and expose the
Observable<Array<string>>instead
of
Array<string>.
@Component({ selector: 'my-app', template: ` <div> <h2>Wikipedia Search</h2> <input type="text" [ngFormControl]="term"/> <ul> <li *ngFor="#item of items | async">{{item}}</li> </ul> </div> ` }) export class App { items: Observable<Array<string>>; term = new Control(); constructor(private wikipediaService: WikipediaService) { this.items = this.term.valueChanges .debounceTime(400) .distinctUntilChanged() .switchMap(term => this.wikipediaService.search(term)); } }
And voilà, here is our final version in a plnkr.
原文链接: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html
相关文章推荐
- [置顶] 跟我学AngularJs:Service、Factory、Provider依赖注入使用与区别
- Angular.js与Bootstrap相结合实现手风琴菜单代码
- angularjs ng-model不能实现双向绑定
- [AngularJS] Angular 1.5 $transclude with named slot
- AngularJS 世界------初识Angularjs
- angular2 Displaying
- [置顶] angularJS中ng-class指令的三种实现方式
- AngularJS for beginners
- angularjs 发送ajax请求的问题
- angular js点击tab中的li标签加载相应div区域
- AngularJs与jquery 差异的分析
- Angularjs的ng-repeat中去除重复的数据
- Angular.js与Bootstrap相结合实现表格分页代码
- AngularJS vs. jQuery,看看谁更胜一筹
- Backbone与Angular的比较
- Angular.js与Bootstrap相结合实现表格分页代码
- 每周小结-3 D3 on AngularJS
- activiti自定义流程之整合(二):使用angular js整合ueditor创建表单
- AngularJS中的按需加载ocLazyLoad
- AngularJS指令