现在主流浏览器都支持XHR和JSONP这两种基于HTTP的API,部分浏览器还支持Fetch。
Angular的HTTP客户端封装了XHR和JSONP API。
还是以Tour of Hero为例。这次我们从服务器获取数据,显示hero列表,能添加新的hero并保存到服务器。
AppComponent
如下:
// app/app.component.ts
import {Component} from 'angular2/core';
import {HTTP_PROVIDERS} from 'angular2/http';
import {Hero} from './hero';
import {HeroListComponent} from './hero-list.component';
import {HeroService} from './hero.service';
@Component({
selector: 'my-app',
template: `
<h1>Tour of Heroes</h1>
<hero-list></hero-list>
`,
directives:[HeroListComponent],
providers: [
HTTP_PROVIDERS,
HeroService,
]
})
export class AppComponent { }
HeroListComponent
如下:
// app/hero-list.component.ts
import {Component, OnInit} from 'angular2/core';
import {Hero} from './hero';
import {HeroService} from './hero.service';
@Component({
selector: 'hero-list',
template: `
<h3>Heroes:</h3>
<ul>
<li *ngFor="#hero of heroes">
{{ hero.name }}
</li>
</ul>
New Hero:
<input #newHero />
<button (click)="addHero(newHero.value); newHero.value=''">
Add Hero
</button>
<div class="error" *ngIf="errorMessage">{{errorMessage}}</div>
`,
styles: ['.error {color:red;}']
})
export class HeroListComponent implements OnInit
constructor (private _heroService: HeroService) {}
errorMessage: string;
heroes:Hero[];
ngOnInit() { this.getHeroes(); }
getHeroes() {
this._heroService.getHeroes()
.subscribe(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
addHero (name: string) {
if (!name) {return;}
this._heroService.addHero(name)
.subscribe(
hero => this.heroes.push(hero),
error => this.errorMessage = <any>error);
}
}
HeroListComponent
组件模板使用NgFor
指令来显示hero列表。在列表下面是一个文本框和Add按钮,我们输入新hero的名称,将它添加到数据库。我们使用本地模板变量newHero
来在(click)
事件绑定中访问文本框。当我们点击Add按钮,我们将该值传递给组件的addHero
方法并清空它。
我们在构造函数中注入了HeroService
。它是由父组件AppComponent
提供的HeroService
的实例。
将HeroService
改成使用HTPP Client,如下:
// app/hero.service.ts
import {Injectable} from 'angular2/core';
import {Http, Response} from 'angular2/http';
import {Hero} from './hero';
import {Observable} from 'rxjs/Observable';
@Injectable()
export class HeroService {
constructor (private http: Http) {}
private _heroesUrl = 'app/heroes'; // URL to web api
getHeroes () {
return this.http.get(this._heroesUrl)
.map(res => <Hero[]> res.json().data)
.catch(this.handleError);
}
private handleError (error: Response) {
// in a real world app, we may send the error to some remote logging infrastructure
// instead of just logging it to the console
console.error(error);
return Observable.throw(error.json().error || 'Server error');
}
}
这里我们将Http
注入到HeroService
的构造函数中。Http
不是在Angular core中,而是在angular2/http
库中,而且它在单独的脚本文件中。因此我们要在index.html
添加:
<script src="node_modules/angular2/bundles/http.dev.js"></script>
接下来我们仔细看看http.get
:
return this.http.get(this._heroesUrl)
.map(res => <Hero[]> res.json().data)
.catch(this.handleError);
我们向get
方法传入URL,它会访问服务器并返回数据。但是返回值貌似跟我们想得有点不一样哎,我们以为返回的会是promise,然后在使用的时候调用then()
来获取数据。但是我们调用的是map()
方法。显然他不是promise。
实际上,http.get
方法返回的是一个HTTP响应的Observable,由RxJS库的提供的Observable 。 map
是一个RxJS的操作符。
RxJS(Reactive Extensions)是个Angular提供的第三方库,实现异步观察模式(asynchronous observable pattern)。
在之前的例子中,我们安装了RxJS npm包,并在index.html
中引用了RxJS脚本。这是使用HTTP client所需要的。
<script src="node_modules/rxjs/bundles/Rx.js"></script>
RxJS非常大,通常只要我们所需要的特性就好了。Angular在rxjs/Observable
模块中提供了简版的Observable
,但是它缺乏我们所需要的所有的操作,包括上面提到的map
方法。
我们在应用启动时引入所有RxJS操作:
// app/main.ts (import rxjs)
// Add all operators to Observable
import 'rxjs/Rx';
看一看我们为什么需要map()
方法。
return this.http.get(this._heroesUrl)
.map(res => <Hero[]> res.json().data)
.catch(this.handleError);
这个response
对象并不包含我们可以直接使用的数据。我们需要调用response.json()
将服务器返回的字节转换成JSON对象。这是因为Angular的HTTP Client使用的规范是ES2015中Fetch
方法返回的response
对象。规范中定义了一个json()
方法将response
正文转换成JSON对象。json()
返回的也不是数组,而是将JSON结果包装在一个对象中,这个对象有个属性data
。这是web api的惯例,基于安全考虑。
getHeroes()
可以直接返回Observable<Response>
。但是这并不是个好主意。数据service的目的就是向用户隐藏与服务器交互的细节。组件调用HeroService
需要的只是数据,而不是response对象。
记住始终进行错误处理。使用Observable的catch
操作处理错误,根据需要决定是否将错误抛到组件中。
回到HeroListComponent
,我们要调用heroService.get
,为subscribe提供第二个函数用于错误处理:
getHeroes() {
this._heroService.getHeroes()
.subscribe(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
在开发的时候我们通常将结果输出到console中。可以使用Observable的do操作,将它放在map
和catch
之间:
return this.http.get(this._heroesUrl)
.map(res => <Hero[]> res.json().data)
.do(data => console.log(data)) // eyeball results in the console
.catch(this.handleError);
在HeroListComponent
中添加一个addHero
方法:
addHero (name: string) : Observable<Hero>
我们的数据服务遵循REST风格,使用POST请求,数据放在请求的body中,就像这样:
{ "name": "Windstorm" }
服务器会生成id,并返回完整的JSON对象。
在HeroService
中实现addHero
:
import {Headers, RequestOptions} from 'angular2/http';
addHero (name: string) : Observable<Hero> {
let body = JSON.stringify({ name });
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this._heroesUrl, body, options)
.map(res => <Hero> res.json().data)
.catch(this.handleError)
}
注意,post
方法的第二个参数是JSON字符串,第三个参数是Content-Type
。
尽管http
API返回的是Observable<Response>
,但是我们可以把它转换成Promise。
// app/toh/hero.service.ts (promise-based)
getHeroes () {
return this.http.get(this._heroesUrl)
.toPromise()
.then(res => <Hero[]> res.json().data, this.handleError)
.then(data => { console.log(data); return data; }); // eyeball results in the console
}
addHero (name: string) : Promise<Hero> {
let body = JSON.stringify({ name });
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this._heroesUrl, body, options)
.toPromise()
.then(res => <Hero> res.json().data)
.catch(this.handleError);
}
private handleError (error: any) {
// in a real world app, we may send the error to some remote logging infrastructure
// instead of just logging it to the console
console.error(error);
return Promise.reject(error.message || error.json().error || 'Server error');
}
// app/toh/hero.service.ts (observable-based)
getHeroes () {
return this.http.get(this._heroesUrl)
.map(res => <Hero[]> res.json().data)
.do(data => console.log(data)) // eyeball results in the console
.catch(this.handleError);
}
addHero (name: string) : Observable<Hero> {
let body = JSON.stringify({ name });
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this._heroesUrl, body, options)
.map(res => <Hero> res.json().data)
.catch(this.handleError)
}
private handleError (error: Response) {
// in a real world app, we may send the error to some remote logging infrastructure
// instead of just logging it to the console
console.error(error);
return Observable.throw(error.json().error || 'Server error');
}
将observable转换成promise只需要调用toPromise(success, fail)
。将observable的map
回调函数移到第一个success参数,catch
回调函数移到第二个fail参数就行了。也可以使用addHero
方法中的then.catch
模式。
在HeroListComponent
中调用:
// app/hero-list.component.ts (promise-based)
getHeroes() {
this._heroService.getHeroes()
.then(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
addHero (name: string) {
if (!name) {return;}
this._heroService.addHero(name)
.then(
hero => this.heroes.push(hero),
error => this.errorMessage = <any>error);
}
// app/hero-list.component.ts (observable-based)
getHeroes() {
this._heroService.getHeroes()
.subscribe(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
addHero (name: string) {
if (!name) {return;}
this._heroService.addHero(name)
.subscribe(
hero => this.heroes.push(hero),
error => this.errorMessage = <any>error);
}
参考资料
Angular2官方文档