HTTP主要是浏览器与服务端的通信协议,流行浏览器支持XHR和JSONP两种基于HTTP API,少数浏览器还需要 Fetch。
Angular2 HTTP客户端封装了 XHR 和 JSONP API。
一、示例
我们使用Angular Http
客户端访问XHR。

依然以前面一直用的 Hero 的示例,只不过这回是从服务端获取、保存数据。一个父组件 TohComponent
和子组件 HeroListComponent
,这和我们之前看到样本示例都一样,只不过改装成从服务端交互。
app/toh/toh.component.ts
import { Component } from 'angular2/core';
import { HTTP_PROVIDERS } from 'angular2/http';
import { HeroListComponent } from './hero-list.component';
import { HeroService } from './hero.service';
@Component({
selector: 'my-toh',
template: `
<h1>Tour of Heroes</h1>
<hero-list></hero-list>
`,
directives: [HeroListComponent],
providers: [
HTTP_PROVIDERS,
HeroService,
]
})
export class TohComponent { }
像往常一样,我们要先导入所需要的模块。这一次要使用Angular HTTP库的 HTTP_PROVIDERS
(如果你依然是用官网示例做为项目种子,要先引用 http.dev.js
);同时还导入 HeroService
这也是我们之前一直在用的用于获取 hero 数据的类。同时还需要注入DI中。
另外 HTTP_PROVIDERS
提供一些基本的HTTP请求操作,比如:Http
、XHRBackend
、RequestOptions
、ResponseOptions
等,所以最佳的方式,可以在Angular2启动时就注册注入:
import { HTTP_PROVIDERS } from 'angular2/http';
bootstrap(TohComponent, [HTTP_PROVIDERS]);
接者 HeroListComponent
组件模板内容:
app/toh/hero-list.component.html
<h3>Heroes:</h3>
<ul>
<li *ngFor="let 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>
首先是一个利用 NgFor
循环指令显示列表。列表的下方是一文本框和Add Hero按钮,可以输入新名称并将它添加到数据库中;使用本地模板变量 newHero
访问值和按钮 (click)
事件绑定,当用户点击按钮时,调用 addHero
组件方法并清空文本框的内容。最下方是一个用于提示错误消息。
app/toh/hero-list.component.ts
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);
}
}
在构造函数里注入 HeroService
,它在父组件 TohComponent
已经被注册过,所以子组件是可以直接被注入。
注意这里并不与服务端有任何直接联系,这一切都委派 heroService
类来处理。
1、黄金法则
始终将数据访问委派给Service类。
2、最佳实践
虽然在组件运行时要请求 heroes 列表,但是我们并没有在构造函数里调用Service的 get
方法。而是利用 ngOnInit
生命周期钩子,当实例化组件时Angular自动调用 ngOnInit
方法。
当其构建函数很简单或需要一些调用远程服务时,最好是使用生命周期钩子,这样更有利于测试和调试。为什么这么说呢?试想如果在构造函数里调用一个远程服务时有异常,那实例到底应该是成功还是失败呢?
组件类的 get
和 addHero
方法返回的 hero 数据是一个 Observable
类型。我们可以订阅它的请求和失败观察者。
现在组件基本上已经弄好了,现在重点要关注 HeroService
客户端与后端数据源的交互。
2、获取数据
系列文章里有很多 HeroService
示例,并且还模拟从服务端请求数据的过程,比如:
import {Injectable} from 'angular2/core';
import {Hero} from './hero';
import {HEROES} from './mock-heroes';
@Injectable()
export class HeroService {
getHeroes() {
return Promise.resolve(HEROES);
}
}
当然,我们现在是要真正从服务器获取 heroes 数据,所以有个全新的 HeroService
类:
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 (): Observable<Hero[]> {
return this.http.get(this._heroesUrl)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
if (res.status < 200 || res.status >= 300) {
throw new Error('Bad response status: ' + res.status);
}
let body = res.json();
return body.data || { };
}
private handleError (error: any) {
// In a real world app, we might send the error to remote logging infrastructure
let errMsg = error.message || 'Server error';
console.error(errMsg); // log to console instead
return Observable.throw(errMsg);
}
}
我们通过加载Angular Http
客户端服务并把它注入到 HeroService
构造函数中。
Http
并不是 angular2/core 的一部分,这是一个可选服务在 angular2/http
库里。此外,该库并不是核心Angular脚本文件里,所以还需要在 index.html
的引入。
<script src="node_modules/angular2/bundles/http.dev.js"></script>
仔细看看我们怎么调用 http.get
:
getHeroes (): Observable<Hero[]> {
return this.http.get(this._heroesUrl)
.map(this.extractData)
.catch(this.handleError);
}
向 get
方法传递资源URL,它会访问服务端并返回 heroes。
返回的结果可能会很惊讶,因为我们会比较期待返回一个 promise,这样我们可以使用 then()
来获取数据,然后我们调用了 map()
方法,而非 promise。
事实上,http.get
方法返回的是一个HTTP响应 Observable 对象,由RxJS库提供,map()
也是RxJS的一个操作符。
3、RxJS库
RxJS是一个第三方库,由Angular赞助,实现异步观察模式。
官网示例默认就有安装 RxJS,并且也在 index.html
里引入脚本,因为Observable模式被广泛在应用在Angular程序。其实早在 Angualr 作者在针对 1.x 被访谈时就已经说过Observable才是未来。
HTTP客户端就需要RxJS,我们必须采用额外的步骤开启RxJS Observables。
4、启用RxJS操作符
RxJS库非常大,当我们构建一个用于移动设备的应用程序时,应该只包括我们实际需要的功能。因此,Angular在 rxjs/Observable
模块中提供一简化版 Observable
,但是缺乏几乎所有操作,包括 getHeroes
中的 map
方法。
我们需要哪些RxJS操作符,按需加载即可。但是现在我们是学习HTTP,不计较这些,所以全部加载吧。
// Add all operators to Observable
import 'rxjs/Rx';
5、处理响应对象
记得,我们的 getHeroes
方法中的 map()
方法,会将提取数据映射到 this.extractData
方法。
private extractData(res: Response) {
if (res.status < 200 || res.status >= 300) {
throw new Error('Bad response status: ' + res.status);
}
let body = res.json();
return body.data || { };
}
response
对象并不是返回我们可以直接使用的数据,要想变成应用程序所需要的数据需要:
错误状态码
示例中的状态码200-300范围从应用角度来说是错误,但对于 http
角度来说并非错误,所以先判断状态码并抛出一个错误。而对于 404 - Not Found
像其他一样会有响应,我们发送一请求出去,然后返回一个响应,这对于 http
来说是错误的,所以会立即得到一个 observable 错误。
因为状态码200-300范围从应用角度来说是错误,所以我们拦截并抛出,移动 observable 链到错误路径。而 catch
操作来处理我们抛出的错误。
解析JSON
响应数据返回的是一个JSON字符串格式,我们必须调用 response.json()
转换成JavaScript对象。这并非Angular自行设计的,这是因为Angular HTTP Client使用的是ES2015规范中的 Fetch
方法返回响应对象,它定义了一个 json()
方法来解析响应内容成JavaScript对象。
JSON劫持
我们不应该让 json()
直接返回一个数组 hero,而应该是返回一个带有 data
属性的对象,比如:{data: [ hero, hero ] }
。
这是因对于老式浏览器可能会有JSON被劫持安全漏洞,这是另外一个话题了,大概就是老式浏览器可以重写 Array.prototype.constructor
从而达到拦截JSON的内容,安全问题不再过多叙述。
不要直接返回response对象
getHeroes()
能够直接返回 Observable<Response>
,但这不是好主意,Server类的目的就是为了向用户隐藏与服务器交互细节,组件只需调用 HeroService
获取想要 hreoes,谁管你什么是Response。
记得处理错误
细心的读者可能已经发现我们使用 catch
操作符并结合 handleError
方法,但并未讨论实际的工作方式,每当我们处理 I/O 时,必须要做到处理错误的准备。
HeroService
捕获错误并返回一个对用户更友好、能理解的消息,handleError
方法是 catch
操作符委托的处理方法。
getHeroes (): Observable<Hero[]> {
return this.http.get(this._heroesUrl)
.map(this.extractData)
.catch(this.handleError);
}
private handleError (error: any) {
// In a real world app, we might send the error to remote logging infrastructure
let errMsg = error.message || 'Server error';
console.error(errMsg); // log to console instead
return Observable.throw(errMsg);
}
二、HeroListComponent的订阅
回到 HeroListComponent
,要调用 heroService.getHeroes
。subscribe
提供的第二个函数用来处理错误消息,并把错误消息赋值给 errorMessage
变量。
getHeroes() {
this._heroService.getHeroes()
.subscribe(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
三、发送数据到服务端
到目前为止,我们使用Angular内置的 Http
服务从远程检索数据,现在该到保存数据到后端了。
我们将在 HeroListComponent
组件创建一个简单的 addHero
方法,只需要一个name(字符串)参数,结果返回一个新 hero 的 observable 类型。
addHero (name: string) : Observable<Hero>
要实现这一点,我们需要了解服务器应用程序接口的一些细节来创建 hero。我们数据服务遵循REST风格,一个端点可以通过请求POST或GET来创建或检索。使用POST请求时,只需要提供一个无 id
属性的 Hero
实体,并把数据放在body里,例如:
{ "name": "Windstorm" }
服务器将生成新 id
并返回包括生成id的整个实体对象,并用一个 data
属性包裹返回响应。
app/toh/hero.service.ts
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(this.extractData)
.catch(this.handleError);
}
post
方法第二个参数需要一个 JSON 字符串,所以这里用 JSON.stringify
将对象转化成字符串。
1、Headers
POST请求需要指明一个 Content-Type
请求体,Headers是一个RequestOptions类型,并作为 post
方法的第三个参数。
app/toh/hero.service.ts
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this._heroesUrl, body, options)
2、JSON结果
如同 getHeroes
,我们使用 json()
从响应中提取数据,也是没有把 hero 包裹至 data
属性。
回到 HeroListComponent
,我们看到了 _heroService.addHero
方法返回一个可被订阅的观察对象,当数据返回时,把新对象推到数组当中。
app/toh/hero-list.component.ts
addHero (name: string) {
if (!name) {return;}
this._heroService.addHero(name)
.subscribe(
hero => this.heroes.push(hero),
error => this.errorMessage = <any>error);
}
3、~~回到Promise~~
虽然Angular http
客户端API返回的是 Observable<Response>
,如果愿意,可以把它变成一个 Promise。
使用Promise重写 HeroService
:
getHeroes (): Promise<Hero[]> {
return this.http.get(this._heroesUrl)
.toPromise()
.then(this.extractData)
.catch(this.handleError);
}
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(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
if (res.status < 200 || res.status >= 300) {
throw new Error('Bad response status: ' + res.status);
}
let body = res.json();
return body.data || { };
}
private handleError (error: any) {
// In a real world app, we might send the error to remote logging infrastructure
let errMsg = error.message || 'Server error';
console.error(errMsg); // log to console instead
return Promise.reject(errMsg);
}
把 Observable 转换成 Promise 最简单就是调用 toPromise(success, fail)
,把observable的 map
回调移到第一个成功参数,catch
回调移到第二个错误参数就行了。或者也可以用 then.catch
模式。
errorHandler
里返回一个失败的promise而非Observable。
然后调整组件使用 Promise
:
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);
}
最明显的区别是,把 subscribe
变成 then
。
从代码角度虽然只是变换了一个单词,但是还是返回的结果有很大的区别。
基于promise的 then
返回的另一个promise,我们可以继续接着链式调用多个 then
和 catch
,每一次都会返回新prromise。
而 subscribe
方法返回的是一个 Subscription
,这对于observable来讲已经是终结,我们不能够再调用 map
或 subscribe
。
四、JSONP
我们只是学会了如何使用内置的 Http
服务创建一个 XMLHttpRequests
,这是服务器通信中最常用的方法,但并非适用所有情况。
出于安全考虑,网页浏览器有一种叫同源策略机制,即无法访问其它域的资源。而流行浏览器允许 XHR
利用CORS协议请求跨域资源共享,这除了浏览器支持外还需要服务端允许。但一些老式浏览器并不支持或服务端因为某些原因无法设置CORS,那么我们需要用 JSONP 来解决只读跨域问题。
1、维基百科搜索
维基百科提供了一个 JSONP
搜索API,让我们弄个简单的维基百科搜索。

Angular的 Jsonp
服务其实是扩展了 Http
服务,并限制只允许 GET
请求,所有其它HTTP方法都会直接抛错,因为JSONP只允许只读请求。
一如既往,我们把所有交互细节都由Service来完成:
app/wiki/wikipedia.service.ts
import {Injectable} from 'angular2/core';
import {Jsonp, URLSearchParams} from 'angular2/http';
@Injectable()
export class WikipediaService {
constructor(private jsonp: Jsonp) {}
search (term: string) {
let wikiUrl = 'http://en.wikipedia.org/w/api.php';
var params = new URLSearchParams();
params.set('search', term); // the user's search value
params.set('action', 'opensearch');
params.set('format', 'json');
params.set('callback', 'JSONP_CALLBACK');
// TODO: Add error handling
return this.jsonp
.get(wikiUrl, { search: params })
.map(request => <string[]> request.json()[1]);
}
}
构造函数注入 jsonp
服务,并且在调用 WikipediaService
所在的组件里注册 JSONP_PROVIDERS
依赖。
2、搜索参数
维基开放搜索API提供四个查询参数(键值对),分别:search
、action
、format
、callback
;其中 search
为于查询关键词,另外三个都是固定值。
如果我们想查询有关 “Angular” 文章,我们可构造这样一个查询字符串,并调用 jsonp
:
let queryString =
`?search=${term}&action=opensearch&format=json&callback=JSONP_CALLBACK`
return this.jsonp
.get(wikiUrl + queryString)
.map(request => <string[]> request.json()[1]);
如果参数很多时,也可以使用Angular URLSearchParams
辅助类。
var params = new URLSearchParams();
params.set('search', term); // the user's search value
params.set('action', 'opensearch');
params.set('format', 'json');
params.set('callback', 'JSONP_CALLBACK');
这次调用 jsonp
,它具有两个参数,wikiUrl
和 一个可选对象做为查询参数。
app/wiki/wikipedia.service.ts
// TODO: Add error handling
return this.jsonp
.get(wikiUrl, { search: params })
.map(request => <string[]> request.json()[1]);
3、WikiComponent组件
现在,已经有一个可以查询维基百科服务接口,我们转向使用用户输入关键词和显示搜索结果的组件。
import {Component} from 'angular2/core';
import {JSONP_PROVIDERS} from 'angular2/http';
import {Observable} from 'rxjs/Observable';
import {WikipediaService} from './wikipedia.service';
@Component({
selector: 'my-wiki',
template: `
<h1>Wikipedia Demo</h1>
<p><i>Fetches after each keystroke</i></p>
<input #term (keyup)="search(term.value)"/>
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>
`,
providers:[JSONP_PROVIDERS, WikipediaService]
})
export class WikiComponent {
constructor (private _wikipediaService: WikipediaService) {}
items: Observable<string[]>;
search (term: string) {
this.items = this._wikipediaService.search(term);
}
}
组件元数据 providers
数组指定了 JSONP_PROVIDERS
用于支持 Jsonp
服务,之前就说过 WikipediaService
里并没有任何注册依赖。
组件提供一个 <input>
搜索框,收集搜索关键词,同时绑定 keyup
事件,每于输入都会调用 search(term)
方法。search(term)
方法委托 WikipediaService
返回一个Observable字符串列表,而不是像我们之前在 HeroListComponent
组件里看到一样返回一个可被订阅的Observable,并在模板里通过 ngFor
异步管道处理订阅。
五、优化
维基百科搜索对服务器的请求太频繁了,这并不是我们最想要。
1、等待用户停止录入
我们绑定的是 keyup
,意味者每一次录入都会发送请求,应用程序应该在用户停止录入时才发送请求:

2、搜索词变化时才发送请求
假设用户在搜索框输入 “Angular”,并暂停一段时间后,应用程序发送请求。然后用户删去三个字母 “lar” 又立即重新录入回去,这时搜索词依然是 “Angular”,应用程序不应该发送请求。
3、处理过期响应
用户录入 angular、停顿、清除搜索框、重新录入 http,应用程序发起 angular 和 http 两次搜索请求。但是我们无法知道哪一次会优先得到响应,有可能是 http 请求优先到达,这时后到达的 angular 覆盖了 http 列表。
当有多个请求时,响应也应该是按顺序响应,像上面如果 angular 是后到达则应该抛弃响应处理。
六、更多Observables玩法
我们可以通过几个漂亮的 observable 操作符来解决这些问题和优化我们的应用程序用户体验,重新更新 WikipediaService
。
import {Component} from 'angular2/core';
import {JSONP_PROVIDERS} from 'angular2/http';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {WikipediaService} from './wikipedia.service';
@Component({
selector: 'my-wiki-smart',
template: `
<h1>Smarter Wikipedia Demo</h1>
<p><i>Fetches when typing stops</i></p>
<input #term (keyup)="search(term.value)"/>
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>
`,
providers:[JSONP_PROVIDERS, WikipediaService]
})
export class WikiSmartComponent {
constructor (private _wikipediaService: WikipediaService) { }
private _searchTermStream = new Subject<string>();
search(term:string) { this._searchTermStream.next(term); }
items:Observable<string[]> = this._searchTermStream
.debounceTime(300)
.distinctUntilChanged()
.switchMap((term:string) => this._wikipediaService.search(term));
}
这里只是更新组件类,元数据和模板都没有任何改动。
1、创建关键词流
搜索框绑定 keyup
事件,所以每次按键都会调用组件 search
方法。
我们使用 Subject
把这些事件变成一个 observable 关键词流,所以需要先加载 RxJS 模块:
import {Subject} from 'rxjs/Subject';
每个关键词都是一个字符串,所以定义一个名为 _searchTermStream
类型为 Subject<string>
私有变量,每次调用 search
方法都会把搜索框值通过 next
方法添加到流当中。
private _searchTermStream = new Subject<string>();
search(term:string) { this._searchTermStream.next(term); }
2、监听关键词
之前我们直接通过Service类请求数据,然后在模块中显示。现在我们改成监听流,并在访问Service请求数据前做一些操作。
items:Observable<string[]> = this._searchTermStream
.debounceTime(300)
.distinctUntilChanged()
.switchMap((term:string) => this._wikipediaService.search(term));
- debounceTime 等待用户输入至少300毫秒时才发送请求。
- distinctUntilChanged 只有变更搜索值才会发送请求。
- switchMap 把Observable产生的结果转换成多个Observable,然后把它扁平化成一个Observable,然依次按顺序把结果给订阅者,虽然会按顺序请求,但是如果请求响应的最新结果早于旧请求结果时,会自动舍弃旧结果的所有操作。
七、总结
Http Client 是一个非常核心库,但是光只是一个HTTP请求库并没有什么特殊的;重点是配合 RxJS,让我们在编写异步操作时使用同步的代码风格,我们也可以叫:响应式编程。