HTTP
继学习笔记10后,可以在路由器中配置相关路由使用户可在不同的视图切换。
在一个实际的项目中,数据使来自远端的服务器上的,在Angular中,Angular通过HttpClient与远端服务器进行通讯。
模拟数据服务器
使用 内存 Web API(In-memory Web API) 模拟出的远程数据服务器通讯(在实际项目中不使用这个模块,在这里只是模拟后端数据)。
a. 安装in-memory-web-api
bogon:demo wjy$ npm install angular-in-memory-web-api --save
+ angular-in-memory-web-api@0.6.1
added 1 package in 51.529s
b.在app.module.ts 导入HttpClientInMemoryWebApiModule,HttpClient
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import {HttpClientModule} from '@angular/common/http';
c.把 HttpClientInMemoryWebApiModule
添加到 @NgModule.imports
数组中(放在 HttpClient
之后), 然后使用 InMemoryDataService
来配置它。
HttpClientModule,
HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, {dataEncapsulation: false}),
forRoot()
配置方法接受一个 InMemoryDataService
类(初期的内存数据库)作为参数。
d.创建一个数据服务:in-memory-data
bogon:demo wjy$ ng generate service InMemoryData
CREATE src/app/in-memory-data.service.spec.ts (412 bytes)
CREATE src/app/in-memory-data.service.ts (141 bytes)
在AppModule中引入InMemoryDataService
import {InMemoryDataService} from './in-memory-data.service';
这个文件代替了之前的 mock-heroes.ts,此时这个时候就可以安全的删除mock-heroes.ts。
当在后期可以从服务器获取数据的时候,就可以删除现在这个文件了。
Hero与HTTP
a.导入一些需要的HTTP符号:
import { HttpClient, HttpHeaders } from '@angular/common/http';
b.把 HttpClient
注入到构造函数中一个名叫 http
的私有属性中。
constructor(
private http: HttpClient,
private messageService: MessageService) { }
c.保留对 MessageService
的注入。你将会频繁调用它,因此请把它包裹进一个私有的 log
方法中。
private log(message: string) {
this.messageService.add(`HeroService: ${message}`);
}
d.把服务器上英雄数据资源的访问地址定义为 heroesURL
。
private heroesUrl = 'api/heroes';
通过 HttpClient 获取英雄
修改当前的 HeroService.getHeroes()
使用 HttpClient 来模拟英雄数据返回为 Observable<Hero[]>
格式。
getHeroes (): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
}
保存,刷新浏览器,应用就会模拟服务器被成功读取。
但是在这里并没有修改查看hero详情的函数,但是在此刻应用并没有报错。这里是因为: of / HttpClient 返回的都是Observable<Hero[]>
。
Http 方法返回单个值
所有的 HttpClient
方法都会返回某个值的 RxJS Observable
。
HTTP 是一个请求/响应式协议。你发起请求,它返回单个的响应。
通常,Observable
可以在一段时间内返回多个值。 但来自 HttpClient
的 Observable
总是发出一个值,然后结束,再也不会发出其它值。
具体到这次 HttpClient.get
调用,它返回一个 Observable<Hero[]>
,在这里“返回的这个Hero数组是可观察对象”。在实践中,它也只会返回一个英雄数组。
HttpClient.get
返回响应数据
HttpClient.get
默认情况下把响应体当做无类型的 JSON 对象进行返回。 如果指定了可选的模板类型 <Hero[]>
,就会给返回你一个类型化的对象。JSON 数据的具体形态是由服务器的数据 API 决定的。 英雄指南的数据 API 会把英雄数据作为一个数组进行返回。
错误处理
有时候在从后端获取数据,可能会因为一些其他原因,发生一些错误。此刻,在HttpClient.get()方法中应该捕获错误,并做适当的处理,如果要捕获错误,就需要
使用 RxJS 的 catchError()
操作符来建立对 Observable 结果的处理管道(pipe)。
从 rxjs/operators
中导入 catchError
符号。
import { catchError} from 'rxjs/operators';
使用 .pipe()
方法来扩展 Observable
的结果,并给它一个 catchError()
操作符
getHeroes (): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
catchError(this.handleError('getHeroes', []))
);
}
此时在this.handleError('getHeroes',[]),这里会报错,是因为,暂时还没有声明这个方法。
catchError()
操作符会拦截失败的 Observable
。 它把错误对象传给错误处理器,错误处理器会处理这个错误。
handleError
在实际的项目中,很多component都需要从远端服务器获取数据,这过程中可能会需要很多的请求,而这么多的请求中都有可能会发生错误,在发生错误的时候,就需要将用类似的 handleError()这种方法,如此之来,handleError() 将会在很多 HeroService
的方法之间共享,所以要把它通用化,以支持这些彼此不同的需求。它不再直接处理这些错误,而是返回给 catchError
返回一个错误处理函数。还要用操作名和出错时要返回的安全值来对这个错误处理函数进行配置。
private handleError<T> (operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
console.error(error); // log to console instead
this.log(`${operation} failed: ${error.message}`);
return of(result as T);
};
}
在这个handleError中,处理错误的方法是:在控制台中汇报了这个错误之后,这个处理器会汇报一个用户友好的消息,并给应用返回一个安全值,使它可以继续工作。因为每个服务方法都会返回不同类型的 Observable
结果,因此 handleError()
也需要一个类型参数,以便它返回一个此类型的安全值,正如应用所期望的那样。
窥探 Observable
HeroService
的方法将会窥探 Observable
的数据流,并通过 log()
函数往页面底部发送一条消息。
它们可以使用 RxJS 的 tap
操作符来实现,该操作符会查看 Observable 中的值,使用那些值做一些事情,并且把它们传出来。 这种 tap
回调不会改变这些值本身。
a.在HeroService中导入:tap
import {catchError, tap} from 'rxjs/operators';
b.修改getHeroes();
getHeroes(): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
tap(heroes => {
console.log(heroes);
this.log('fetched heroes');
}),
catchError(this.handleError('getHeroes', []))
);
}
在这里可以打印出来获取到的heroes;在控制台可以看到如下信息:
通过ID去获取hero信息
在实际项目中,有时候会遇到一个某种一系列类似事物的列表,然后要查看这个一系列中的某一个的详细信息,此时可能就会需要类似于通过 api/hero/:id
的形式(比如 api/hero/:id
)获取这个单个对象的全部信息,修改 HeroService.getHero()
方法来发起请求
getHero(id: number): Observable<Hero> {
const url = `${this.heroesUrl}/${id}`;
return this.http.get<Hero>(url).pipe(
tap(_ => this.log(`fetched hero id=${id}`)),
catchError(this.handleError<Hero>(`getHero id=${id}`))
);
}
这里的getHeroes()和修改前的
相比有三个显著的差异。
-
它使用想获取的英雄的 id 构建了一个请求 URL。
-
服务器应该使用单个英雄作为回应,而不是一个英雄数组。
-
所以,
getHero
会返回Observable<Hero>
(“一个可观察的单个hero对象”),而不是一个可观察的hero对象数组。
非常抱歉,丢掉了一段代码,以下是InMemoryDataService 的代码,这个服务实现了InMemoryDbService的接口,注意,这里一定要实现createDb()这个方法。
import {Injectable} from '@angular/core';
import {InMemoryDbService} from 'angular-in-memory-web-api';
@Injectable({
providedIn: 'root'
})
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{id: 11, name: 'Dr Nice'},
{id: 12, name: 'Narco'},
{id: 13, name: 'Bombasto'},
{id: 14, name: 'Celeritas'},
{id: 15, name: 'Magneta'},
{id: 16, name: 'RubberMan'},
{id: 17, name: 'Dynama'},
{id: 18, name: 'Dr IQ'},
{id: 19, name: 'Magma'},
{id: 20, name: 'Tornado'}
];
return {heroes};
}
genId(heroes: Hero[]): number {
return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11;
}
}