1,Http 方法返回单个值
所有的 HttpClient
方法都会返回某个值的 RxJS Observable
。
HTTP 是一个请求/响应式协议。你发起请求,它返回单个的响应。
通常,Observable
可以在一段时间内返回多个值。 但来自 HttpClient
的 Observable
总是发出一个值,然后结束,再也不会发出其它值。
具体到这次 HttpClient.get
调用,它返回一个 Observable<Hero[]>
,顾名思义就是“一个英雄数组的可观察对象”。在实践中,它也只会返回一个英雄数组。
HttpClient.get
返回响应数据
HttpClient.get
默认情况下把响应体当做无类型的 JSON 对象进行返回。 如果指定了可选的模板类型 <Hero[]>
,就会给返回你一个类型化的对象。
JSON 数据的具体形态是由服务器的数据 API 决定的
错误处理
凡事皆会出错,特别是当你从远端服务器获取数据的时候。 HeroService.getHeroes()
方法应该捕获错误,并做适当的处理。
要捕获错误,你就要使用 RxJS 的 catchError()
操作符来建立对 Observable 结果的处理管道(pipe)。
从 rxjs/operators
中导入 catchError
符号,以及你稍后将会用到的其它操作符。
import { catchError, map, tap } from 'rxjs/operators';
窥探 Observable
HeroService
的方法将会窥探 Observable
的数据流,并通过 log()
函数往页面底部发送一条消息。
它们可以使用 RxJS 的 tap
操作符来实现,该操作符会查看 Observable 中的值,使用那些值做一些事情,并且把它们传出来。 这种 tap
回调不会改变这些值本身。
/** GET heroes from the server */
getHeroes (): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
tap(_ => this.log('fetched heroes')),
catchError(this.handleError<Hero[]>('getHeroes', []))
);
}
修改英雄
添加如下的 save()
方法,它使用英雄服务中的 updateHero()
方法来保存对英雄名字的修改,然后导航回前一个视图。
save(): void {
this.heroService.updateHero(this.hero)
.subscribe(() => this.goBack());
}
//保存到服务器
/** PUT: update the hero on the server */
updateHero (hero: Hero): Observable<any> {
return this.http.put(this.heroesUrl, hero, httpOptions).pipe(
tap(_ => this.log(`updated hero id=${hero.id}`)),
catchError(this.handleError<any>('updateHero'))
);
}
往 HeroService
类中添加 addHero()
方法。
/** POST: add a new hero to the server */
addHero (hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions).pipe(
tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
catchError(this.handleError<Hero>('addHero'))
);
}
eroService.addHero()
和 updateHero
有两点不同。
-
它调用
HttpClient.post()
而不是put()
。 -
它期待服务器为这个新的英雄生成一个 id,然后把它通过
Observable<Hero>
返回给调用者。
添加新英雄
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.addHero({ name } as Hero)
.subscribe(hero => {
this.heroes.push(hero);
});
}
添加 HeroService.addHero()
往 HeroService
类中添加 addHero()
方法。:
/** POST: add a new hero to the server */
addHero (hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions).pipe(
tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
catchError(this.handleError<Hero>('addHero'))
);
}
删除某个英雄
delete(hero: Hero): void {
this.heroes = this.heroes.filter(h => h !== hero);
this.heroService.deleteHero(hero).subscribe();
}
虽然这个组件把删除英雄的逻辑委托给了 HeroService
,但仍保留了更新它自己的英雄列表的职责。 组件的 delete()
方法会在 HeroService
对服务器的操作成功之前,先从列表中移除要删除的英雄。
组件与 heroService.delete()
返回的 Observable
还完全没有关联。必须订阅它。
如果你忘了调用 subscribe()
,本服务将不会把这个删除请求发送给服务器。 作为一条通用的规则,Observable
在有人订阅之前什么都不会做。
/** DELETE: delete the hero from the server */
deleteHero (hero: Hero | number): Observable<Hero> {
const id = typeof hero === 'number' ? hero : hero.id;
const url = `${this.heroesUrl}/${id}`;
return this.http.delete<Hero>(url, httpOptions).pipe(
tap(_ => this.log(`deleted hero id=${id}`)),
catchError(this.handleError<Hero>('deleteHero'))
);
}
注意
-
它调用了
HttpClient.delete
。 -
URL 就是英雄的资源 URL 加上要删除的英雄的
id
。 -
你不用像
put
和post
中那样发送任何数据。 -
你仍要发送
httpOptions
。
AsyncPipe
<li *ngFor="let hero of heroes$ | async" >
仔细看,你会发现 *ngFor
是在一个名叫 heroes$
的列表上迭代,而不是 heroes
。
$
是一个命名惯例,用来表明 heroes$
是一个 Observable
,而不是数组。
*ngFor
不能直接使用 Observable
。 不过,它后面还有一个管道字符(|
),后面紧跟着一个 async
,它表示 Angular 的 AsyncPipe
。
AsyncPipe
会自动订阅到 Observable
,这样你就不用再在组件类中订阅了。
import { Component, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import {
debounceTime, distinctUntilChanged, switchMap
} from 'rxjs/operators';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-hero-search',
templateUrl: './hero-search.component.html',
styleUrls: [ './hero-search.component.css' ]
})
export class HeroSearchComponent implements OnInit {
heroes$: Observable<Hero[]>;
private searchTerms = new Subject<string>();
constructor(private heroService: HeroService) {}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
ngOnInit(): void {
this.heroes$ = this.searchTerms.pipe(
// wait 300ms after each keystroke before considering the term
debounceTime(300),
// ignore new term if same as previous term
distinctUntilChanged(),
// switch to new search observable each time the term changes
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
}
}
RxJS Subject
类型的 searchTerms
searchTerms
属性声明成了 RxJS 的 Subject
类型。
Subject
既是可观察对象的数据源,本身也是 Observable
。 你可以像订阅任何 Observable
一样订阅 Subject
。
你还可以通过调用它的 next(value)
方法往 Observable
中推送一些值,就像 search()
方法中一样。
search()
是通过对文本框的 keystroke
事件的事件绑定来调用的。
<input #searchBox id="search-box" (input)="search(searchBox.value)" />
串联 RxJS 操作符
如果每当用户击键后就直接调用 searchHeroes()
将导致创建海量的 HTTP 请求,浪费服务器资源并消耗大量网络流量。
应该怎么做呢?ngOnInit()
往 searchTerms
这个可观察对象的处理管道中加入了一系列 RxJS 操作符,用以缩减对 searchHeroes()
的调用次数,并最终返回一个可及时给出英雄搜索结果的可观察对象(每次都是 Hero[]
)。
this.heroes$ = this.searchTerms.pipe(
// wait 300ms after each keystroke before considering the term
debounceTime(300),
// ignore new term if same as previous term
distinctUntilChanged(),
// switch to new search observable each time the term changes
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
-
在传出最终字符串之前,
debounceTime(300)
将会等待,直到新增字符串的事件暂停了 300 毫秒。 你实际发起请求的间隔永远不会小于 300ms。 -
distinctUntilChanged()
会确保只在过滤条件变化时才发送请求。 -
switchMap()
会为每个从debounce
和distinctUntilChanged
中通过的搜索词调用搜索服务。 它会取消并丢弃以前的搜索可观察对象,只保留最近的。
借助 switchMap 操作符, 每个有效的击键事件都会触发一次 HttpClient.get()
方法调用。 即使在每个请求之间都有至少 300ms 的间隔,仍然可能会同时存在多个尚未返回的 HTTP 请求。
switchMap()
会记住原始的请求顺序,只会返回最近一次 HTTP 方法调用的结果。 以前的那些请求都会被取消和舍弃。
注意,取消前一个 searchHeroes()
可观察对象并不会中止尚未完成的 HTTP 请求。 那些不想要的结果只会在它们抵达应用代码之前被舍弃。