HTTP 客户端

HTTP is the primary protocol for browser/server communication.

HTTP 是浏览器和服务器之间通讯的主要协议。

The WebSocket protocol is another important communication technology; it isn't covered in this page.


Modern browsers support two HTTP-based APIs: XMLHttpRequest (XHR) and JSONP. A few browsers also support Fetch.

现代浏览器支持两种基于 HTTP 的 API: XMLHttpRequest (XHR)JSONP。少数浏览器还支持 Fetch

The Angular HTTP library simplifies application programming with the XHR and JSONP APIs.

Angular的HTTP库,简化了使用XHRJSONP API的编程方式。



A live example illustrates these topics.




This page describes server communication with the help of the following demos:


The root AppComponent orchestrates these demos:



import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: ` <hero-list></hero-list> <hero-list-promise></hero-list-promise> <my-wiki></my-wiki> <my-wiki-smart></my-wiki-smart> ` }) export class AppComponent { }

Providing HTTP services

提供 HTTP 服务

First, configure the application to use server communication facilities.


The Angular Http client communicates with the server using a familiar HTTP request/response protocol. The Http client is one of a family of services in the Angular HTTP library.

我们通过 Http 客户端,使用熟悉的 HTTP 请求/回应协议与服务器通讯。 Http客户端是Angular的HTTP库所提供的一系列服务之一。

When importing from the @angular/http module, SystemJS knows how to load services from the Angular HTTP library because the systemjs.config.js file maps to that module name.

当我们从@angular/http模块中导入服务时,SystemJS 知道该如何从Angular的HTTP库中加载它们, 这是因为systemjs.config.js文件已经注册过这个模块名。

Before you can use the Http client, you need to register it as a service provider with the dependency injection system.


Read about providers in the Dependency Injection page.


Register providers by importing other NgModules to the root NgModule in app.module.ts.

app.module.ts中通过导入其他模块来注册提供商到根 NgModule。

src/app/app.module.ts (v1)

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { HttpModule, JsonpModule } from '@angular/http'; import { AppComponent } from './app.component'; @NgModule({ imports: [ BrowserModule, FormsModule, HttpModule, JsonpModule ], declarations: [ AppComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }

Begin by importing the necessary members. The newcomers are the HttpModule and the JsonpModule from the Angular HTTP library. For more information about imports and related terminology, see the MDN reference on the import statement.

我们从导入所需的成员开始,它们中的大多数我们都熟悉了,只有HttpModuleJsonpModule是新面孔。 关于导入和相关术语的更多信息,见 MDN reference 中的import语句。

To add these modules to the application, pass them to the imports array in the root @NgModule.


The HttpModule is necessary for making HTTP calls. Though the JsonpModule isn't necessary for plain HTTP, there is a JSONP demo later in this page. Loading its module now saves time.

我们需要 HttpModule 来发起 HTTP 调用。 普通的 HTTP 调用并不需要用到 JsonpModule, 不过稍后我们就会演示对 JSONP 的支持, 所以现在就加载它,免得再回来改浪费时间。

The Tour of Heroes HTTP client demo

《英雄指南》的 HTTP 客户端演示

The first demo is a mini-version of the tutorial's "Tour of Heroes" (ToH) application. This version gets some heroes from the server, displays them in a list, lets the user add new heroes, and saves them to the server. The app uses the Angular Http client to communicate via XMLHttpRequest (XHR).

我们的第一个演示是《英雄指南(TOH)》教程的一个迷你版。 这个版本从服务器获取一些英雄,把它们显示在列表中,还允许我们添加新的英雄并将其保存到服务器。 借助 Angular 的 Http 客户端,我们通过XMLHttpRequest (XHR)与服务器通讯。

It works like this:


ToH mini app

This demo has a single component, the HeroListComponent. Here's its template:



<h1>Tour of Heroes ({{mode}})</h1> <h3>Heroes:</h3> <ul> <li *ngFor="let hero of heroes">{{}}</li> </ul> <label>New hero name: <input #newHeroName /></label> <button (click)="addHero(newHeroName.value); newHeroName.value=''">Add Hero</button> <p class="error" *ngIf="errorMessage">{{errorMessage}}</p>

It presents the list of heroes with an ngFor. Below the list is an input box and an Add Hero button where you can enter the names of new heroes and add them to the database. A template reference variable, newHeroName, accesses the value of the input box in the (click) event binding. When the user clicks the button, that value is passed to the component's addHero method and then the event binding clears it to make it ready for a new hero name.

它使用ngFor来展现这个英雄列表。 列表的下方是一个输入框和一个 Add Hero 按钮,在那里,我们可以输入新英雄的名字, 并把它们加到数据库中。 在(click)事件绑定中,使用模板引用变量newHeroName来访问这个输入框的值。 当用户点击此按钮时,这个值传给组件的addHero方法,然后清除它,以备输入新英雄的名字。

Below the button is an area for an error message.


The HeroListComponent class


Here's the component class:


src/app/toh/hero-list.component.ts (class)

export class HeroListComponent implements OnInit { errorMessage: string; heroes: Hero[]; mode = 'Observable'; constructor (private heroService: HeroService) {} 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.create(name) .subscribe( hero => this.heroes.push(hero), error => this.errorMessage = <any>error); } }

Angular injects a HeroService into the constructor and the component calls that service to fetch and save data.


The component does not talk directly to the Angular Http client. The component doesn't know or care how it gets the data. It delegates to the HeroService.

这个组件不会直接和 Angular 的 Http 客户端打交道! 它既不知道也不关心我们如何获取数据,这些都被委托给了HeroService去做。

This is a golden rule: always delegate data access to a supporting service class.


Although at runtime the component requests heroes immediately after creation, you don't call the service's get method in the component's constructor. Instead, call it inside the ngOnInit lifecycle hook and rely on Angular to call ngOnInit when it instantiates this component.

虽然在运行期间,组件会在创建之后立刻请求这些英雄数据, 但我们在组件的构造函数中调用此服务的get方法。 而是在ngOnInit生命周期钩子中调用它, Angular 会在初始化该组件时调用ngOnInit方法。

This is a best practice. Components are easier to test and debug when their constructors are simple, and all real work (especially calling a remote server) is handled in a separate method.

这是最佳实践。 当组件的构造函数足够简单,并且所有真实的工作(尤其是调用远端服务器) 都在一个独立的方法中处理时,组件会更加容易测试和调试。

The service's getHeroes() and create() methods return an Observable of hero data that the Angular Http client fetched from the server.

服务的getHeroes()addHero()方法返回一个英雄数据的可观察对象 (Observable), 这些数据是由 Angular 的 Http 客户端从服务器上获取的。

Think of an Observable as a stream of events published by some source. To listen for events in this stream, subscribe to the Observable. These subscriptions specify the actions to take when the web request produces a success event (with the hero data in the event payload) or a fail event (with the error in the payload).

我们可以把可观察对象Observable看做一个由某些“源”发布的事件流。 通过订阅到可观察对象Observable,我们监听这个流中的事件。 在这些订阅中,我们指定了当 Web 请求生成了一个成功事件(有效载荷是英雄数据) 或失败事件(有效载荷是错误对象)时该如何采取行动。

With a basic understanding of the component, you're ready to look inside the HeroService.


Fetch data with http.get()

通过 http.get() 获取数据

In many of the previous samples the app faked the interaction with the server by returning mock heroes in a service like this one:


import { Injectable } from '@angular/core'; import { Hero } from './hero'; import { HEROES } from './mock-heroes'; @Injectable() export class HeroService { getHeroes(): Promise<Hero[]> { return Promise.resolve(HEROES); } }

You can revise that HeroService to get the heroes from the server using the Angular Http client service:

在本章中,我们会修改HeroService,改用 Angular 的 Http 客户端来从服务器上获取英雄列表:

src/app/toh/hero.service.ts (revised)

import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/map'; import { Hero } from './hero'; @Injectable() export class HeroService { private heroesUrl = 'api/heroes'; // URL to web API constructor (private http: Http) {} getHeroes(): Observable<Hero[]> { return this.http.get(this.heroesUrl) .map(this.extractData) .catch(this.handleError); } private extractData(res: Response) { let body = res.json(); return || { }; } private handleError (error: Response | any) { // In a real world app, you might use a remote logging infrastructure let errMsg: string; if (error instanceof Response) { const body = error.json() || ''; const err = body.error || JSON.stringify(body); errMsg = `${error.status} - ${error.statusText || ''} ${err}`; } else { errMsg = error.message ? error.message : error.toString(); } console.error(errMsg); return Observable.throw(errMsg); } }

Notice that the Angular Http client service is injected into the HeroService constructor.

注意,这个 Angular Http 客户端服务被注入到了HeroService的构造函数中。

constructor (private http: Http) {}

Look closely at how to call http.get:


src/app/toh/hero.service.ts (getHeroes)

getHeroes(): Observable<Hero[]> { return this.http.get(this.heroesUrl) .map(this.extractData) .catch(this.handleError); }

You pass the resource URL to get and it calls the server which returns heroes.

我们把资源的 URL 传进get函数,它调用了服务器,而服务器应该返回英雄列表。

The server returns heroes once you've set up the in-memory web api described in the appendix below. Alternatively, you can temporarily target a JSON file by changing the endpoint URL:

一旦我们按附录中所描述的那样准备好了内存 Web API,它将返回英雄列表。 但目前,你可以临时性地使用一个 JSON 文件,修改一下 URL:

private heroesUrl = 'app/heroes.json'; // URL to JSON file

If you are familiar with asynchronous methods in modern JavaScript, you might expect the get method to return a promise. You'd expect to chain a call to then() and extract the heroes. Instead you're calling a map() method. Clearly this is not a promise.

返回值可能会让我们感到意外。 对熟悉现代 JavaScript 中的异步调用方法的人来说,我们期待get方法返回一个承诺 (promise)。 我们期待链接调用then()方法,并从中取得英雄列表。 而这里调用了一个map()方法,显然,它不是承诺 (Promise)。

In fact, the http.get method returns an Observable of HTTP Responses (Observable<Response>) from the RxJS library and map() is one of the RxJS operators.

事实上,http.get方法返回了一个 HTTP Response 类型的可观察对象 (Observable<Response>),这个对象来自 RxJS 库,而map()是 RxJS 的操作符之一。

RxJS library


RxJS is a third party library, endorsed by Angular, that implements the asynchronous Observable pattern.


All of the Developer Guide samples have installed the RxJS npm package because Observables are used widely in Angular applications. This app needs it when working with the HTTP client. But you must take a critical extra step to make RxJS Observables usable: you must import the RxJS operators individually.

开发指南中的所有例子都安装了 RxJS 的 npm 包, 这是因为可观察对象在 Angular 应用中使用非常广泛。 HTTP 客户端更需要它。但还要经过一个关键步骤,我们才能用 RxJS 可观察对象: 我们必须单独导入一些RxJS的操作符。

Enable RxJS operators

启用 RxJS 操作符

The RxJS library is large. Size matters when building a production application and deploying it to mobile devices. You should include only necessary features.

RxJS 库实在是太大了。 当构建一个产品级应用,并且把它发布到移动设备上的时候,大小就会成为一个问题。 我们应该只包含那些我们确实需要的特性。

Each code file should add the operators it needs by importing from an RxJS library. The getHeroes() method needs the map() and catch() operators so it imports them like this.

每个代码文件都需要把它需要的操作符从RxJS库中导入,并添加进来。 getHeroes()方法需要一个map()和一个catch()操作符,那就像这样导入它:

src/app/app.component.ts (import rxjs)

import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/map';

Process the response object


Remember that the getHeroes() method used an extractData() helper method to map the http.get response object to heroes:


src/app/toh/hero.service.ts (excerpt)

private extractData(res: Response) { let body = res.json(); return || { }; }

The response object doesn't hold the data in a form the app can use directly. You must parse the response data into a JSON object.

这个response对象并没有以一种我们能直接使用的格式来保存数据。 要让它在应用程序中可用,我们就必须把这个响应数据解析成一个 JSON 对象。

Parse to JSON

解析成 JSON

The response data are in JSON string form. The app must parse that string into JavaScript objects by calling response.json().

响应数据是 JSON 字符串格式的。 我们必须把这个字符串解析成 JavaScript 对象 —— 只要调一下response.json()就可以了。

This is not Angular's own design. The Angular HTTP client follows the Fetch specification for the response object returned by the Fetch function. That spec defines a json() method that parses the response body into a JavaScript object.

这不是 Angular 自己的设计。 Angular HTTP 客户端遵循 ES2015 规范来处理Fetch函数返回的响应对象。 此规范中定义了一个json()函数,来把响应体解析成 JavaScript 对象。

Don't expect the decoded JSON to be the heroes array directly. This server always wraps JSON results in an object with a data property. You have to unwrap it to get the heroes. This is conventional web API behavior, driven by security concerns.

我们不应该期待解码后的 JSON 直接就是一个英雄数组。 调用的这个服务器总会把 JSON 结果包装进一个带data属性的对象中。 我们必须解开它才能得到英雄数组。这是一个约定俗成的 Web API 行为规范,它是出于 安全方面的考虑

Make no assumptions about the server API. Not all servers return an object with a data property.

不要对服务端 API 做任何假设。 并非所有服务器都会返回一个带data属性的对象。

Do not return the response object


The getHeroes() method could have returned the HTTP response but this wouldn't follow best practices. The point of a data service is to hide the server interaction details from consumers. The component that calls the HeroService only wants heroes and is kept separate from getting them, the code dealing with where they come from, and the response object.

getHeroes()确实可以返回 HTTP 响应对象,但这不是最佳实践。 数据服务的重点在于,对消费者隐藏与服务器交互的细节。 调用HeroService的组件希望得到英雄数组。 它并不关心我们如何得到它们。 它也不在乎这些数据从哪里来。 毫无疑问,它也不希望直接和一个响应对象打交道。

HTTP GET is delayed
HTTP 的 GET 方法被推迟执行了

The http.get does not send the request just yet. This Observable is cold, which means that the request won't go out until something subscribes to the Observable. That something is the HeroListComponent.

http.get仍然没有发送请求!这是因为可观察对象是 冷的, 也就是说,只有当某人订阅了这个可观察对象时,这个请求才会被发出。 这个场景中的某人就是HeroListComponent

Always handle errors


An important part of dealing with I/O is anticipating errors by preparing to catch them and do something with them. One way to handle errors is to pass an error message back to the component for presentation to the user, but only if it says something that the user can understand and act upon.

一旦开始与 I/O 打交道,我们就必须准备好接受墨菲定律:如果一件倒霉事可能发生,它就迟早会发生。 我们可以在HeroService中捕获错误,并对它们做些处理。 只有在用户可以理解并采取相应行动的时候,我们才把错误信息传回到组件,让组件展示给最终用户。

This simple app conveys that idea, albeit imperfectly, in the way it handles a getHeroes error.


src/app/toh/hero.service.ts (excerpt)

getHeroes(): Observable<Hero[]> { return this.http.get(this.heroesUrl) .map(this.extractData) .catch(this.handleError); } private handleError (error: Response | any) { // In a real world app, you might use a remote logging infrastructure let errMsg: string; if (error instanceof Response) { const body = error.json() || ''; const err = body.error || JSON.stringify(body); errMsg = `${error.status} - ${error.statusText || ''} ${err}`; } else { errMsg = error.message ? error.message : error.toString(); } console.error(errMsg); return Observable.throw(errMsg); }

The catch() operator passes the error object from http to the handleError() method. The handleError method transforms the error into a developer-friendly message, logs it to the console, and returns the message in a new, failed Observable via Observable.throw.

catch()操作符将错误对象传递给httphandleError()方法。 服务处理器 (handleError) 把响应对象记录到控制台中, 把错误转换成对用户友好的消息,并且通过Observable.throw来把这个消息放进一个新的、用于表示“失败”的可观察对象。

app/toh/hero.service.ts (excerpt)

出错的文件: ../../../_fragments/server-communication/ts/app/toh/ 所在路径: docs,ts,latest,guide,server-communication 文档路径: ../../../

HeroListComponent error handling

HeroListComponent 错误处理

Back in the HeroListComponent, in heroService.getHeroes(), the subscribe function has a second function parameter to handle the error message. It sets an errorMessage variable that's bound conditionally in the HeroListComponent template.

回到HeroListComponent,这里我们调用了heroService.getHeroes()。我们提供了subscribe函数的第二个参数来处理错误信息。 它设置了一个errorMessage变量,被有条件的绑定到了HeroListComponent模板中。

src/app/toh/hero-list.component.ts (getHeroes)

getHeroes() { this.heroService.getHeroes() .subscribe( heroes => this.heroes = heroes, error => this.errorMessage = <any>error); }

Want to see it fail? In the HeroService, reset the api endpoint to a bad value. Afterward, remember to restore it.

想看到它失败时的情况吗?在HeroService中把 API 的端点设置为一个无效值就行了。但别忘了恢复它。

Send data to the server


So far you've seen how to retrieve data from a remote location using an HTTP service. Now you'll add the ability to create new heroes and save them in the backend.

前面我们已经看到如何用一个 HTTP 服务从远端获取数据了。 但我们还能再给力一点,让它可以创建新的英雄,并把它们保存到后端。

You'll write a method for the HeroListComponent to call, a create() method, that takes just the name of a new hero and returns an Observable of Hero. It begins like this:


create(name: string): Observable<Hero> {

To implement it, you must know the server's API for creating heroes.

要实现它,我们得知道关于服务端 API 如何创建英雄的一些细节。

This sample's data server follows typical REST guidelines. It expects a POST request at the same endpoint as GET heroes. It expects the new hero data to arrive in the body of the request, structured like a Hero entity but without the id property. The body of the request should look like this:

我们的数据服务器遵循典型的 REST 指导原则。 它期待在和GET英雄列表的同一个端点上存在一个POST请求。 它期待从请求体 (body) 中获得新英雄的数据,数据的结构和Hero对象相同,但是不带id属性。 请求体应该是这样的:

{ "name": "Windstorm" }

The server generates the id and returns the entire JSON representation of the new hero including its generated id. The hero arrives tucked inside a response object with its own data property.

服务器将生成id,并且返回新英雄的完整JSON形式,包括这个生成的 id。 该英雄的数据被塞进一个响应对象的data属性中。

Now that you know how the API works, implement create() as follows:

现在,知道了这个 API 如何工作,我们就可以像这样实现create()了:

src/app/toh/hero.service.ts (additional imports)

import { Headers, RequestOptions } from '@angular/http';

src/app/toh/hero.service.ts (create)

create(name: string): Observable<Hero> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); return, { name }, options) .map(this.extractData) .catch(this.handleError); }


请求头 (headers)

In the headers object, the Content-Type specifies that the body represents JSON.

我们通过Content-Type头告诉服务器,body 是 JSON 格式的。

Next, the headers object is used to configure the options object. The options object is a new instance of RequestOptions, a class that allows you to specify certain settings when instantiating a request. In this way, headers is one of the RequestOptions.

接下来,使用headers对象来配置options对象。 options对象是RequestOptions的新实例,该类允许你在实例化请求时指定某些设置。这样, HeadersRequestOptions 中的一员。

In the return statement, options is the third argument of the post() method, as shown above.


JSON results


As with getHeroes(), use the extractData() helper to extract the data from the response.


Back in the HeroListComponent, its addHero() method subscribes to the Observable returned by the service's create() method. When the data arrive it pushes the new hero object into its heroes array for presentation to the user.

回到HeroListComponent,我们看到该组件的addHero()方法中订阅了这个由服务中create()方法返回的可观察对象。 当有数据到来时,它就会把这个新的英雄对象追加 (push) 到heroes数组中,以展现给用户。

src/app/toh/hero-list.component.ts (addHero)

addHero(name: string) { if (!name) { return; } this.heroService.create(name) .subscribe( hero => this.heroes.push(hero), error => this.errorMessage = <any>error); }

Fall back to promises

倒退为承诺 (Promise)

Although the Angular http client API returns an Observable<Response> you can turn it into a Promise<Response>. It's easy to do, and in simple cases, a Promise-based version looks much like the Observable-based version.

虽然 Angular 的http客户端 API 返回的是Observable<Response>类型的对象,但我们也可以把它转成 Promise<Response>。 这很容易,并且在简单的场景中,一个基于承诺 (Promise) 的版本看起来很像基于可观察对象 (Observable) 的版本。

While Promises may be more familiar, Observables have many advantages.


Here is a comparison of the HeroService using Promises versus Observables, highlighting just the parts that are different.


getHeroes (): Promise<Hero[]> { return this.http.get(this.heroesUrl) .toPromise() .then(this.extractData) .catch(this.handleError); } addHero (name: string): Promise<Hero> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); return, { name }, options) .toPromise() .then(this.extractData) .catch(this.handleError); } private extractData(res: Response) { let body = res.json(); return || { }; } private handleError (error: Response | any) { // In a real world app, we might use a remote logging infrastructure let errMsg: string; if (error instanceof Response) { const body = error.json() || ''; const err = body.error || JSON.stringify(body); errMsg = `${error.status} - ${error.statusText || ''} ${err}`; } else { errMsg = error.message ? error.message : error.toString(); } console.error(errMsg); return Promise.reject(errMsg); } getHeroes(): Observable<Hero[]> { return this.http.get(this.heroesUrl) .map(this.extractData) .catch(this.handleError); } create(name: string): Observable<Hero> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); return, { name }, options) .map(this.extractData) .catch(this.handleError); } private extractData(res: Response) { let body = res.json(); return || { }; } private handleError (error: Response | any) { // In a real world app, you might use a remote logging infrastructure let errMsg: string; if (error instanceof Response) { const body = error.json() || ''; const err = body.error || JSON.stringify(body); errMsg = `${error.status} - ${error.statusText || ''} ${err}`; } else { errMsg = error.message ? error.message : error.toString(); } console.error(errMsg); return Observable.throw(errMsg); }

You can follow the Promise then(this.extractData).catch(this.handleError) pattern as in this example.


Alternatively, you can call toPromise(success, fail). The Observable's map callback moves to the first success parameter and its catch callback to the second fail parameter in this pattern: .toPromise(this.extractData, this.handleError).

另外,你也可以调用toPromise(success, fail)。可观察对象的map第一个参数为成功时的回调函数, 它的第二个参数用.toPromise(this.extractData, this.handleError)来拦截失败。

The errorHandler forwards an error message as a failed Promise instead of a failed Observable.


The diagnostic log to console is just one more then() in the Promise chain.


You have to adjust the calling component to expect a Promise instead of an Observable:


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); } getHeroes() { this.heroService.getHeroes() .subscribe( heroes => this.heroes = heroes, error => this.errorMessage = <any>error); } addHero(name: string) { if (!name) { return; } this.heroService.create(name) .subscribe( hero => this.heroes.push(hero), error => this.errorMessage = <any>error); }

The only obvious difference is that you call then() on the returned Promise instead of subscribe. Both methods take the same functional arguments.

唯一一个比较明显的不同点是我们调用这个返回的承诺的then()方法,而不再是subscribe。 我们给了这两个方法完全相同的调用参数。

The less obvious but critical difference is that these two methods return very different results.


The Promise-based then() returns another Promise. You can keep chaining more then() and catch() calls, getting a new promise each time.


The subscribe() method returns a Subscription. A Subscription is not another Observable. It's the end of the line for Observables. You can't call map() on it or call subscribe() again. The Subscription object has a different purpose, signified by its primary method, unsubscribe.

subscribe()方法返回一个Subscription对象。但Subscription不是另一个Observable。 它是可观察对象的末端。我们不能在它上面调用map()函数或再次调用subscribe()函数。 Subscription对象的设计目的是不同的,这从它的主方法unsubscribe就能看出来。

To understand the implications and consequences of subscriptions, watch Ben Lesh's talk on Observables or his video course on

要理解订阅的实现和效果,请看 Ben Lesh 关于可观察对象的演讲 或者他在 的课程。

Cross-Origin Requests: Wikipedia example

跨域请求:Wikipedia 范例

You just learned how to make XMLHttpRequests using the Angular Http service. This is the most common approach to server communication, but it doesn't work in all scenarios.

我们刚刚学习了用 Angular Http 服务发起XMLHttpRequests。 这是与服务器通讯时最常用的方法。 但它不适合所有场景。

For security reasons, web browsers block XHR calls to a remote server whose origin is different from the origin of the web page. The origin is the combination of URI scheme, hostname, and port number. This is called the same-origin policy.

出于安全的考虑,网络浏览器会阻止调用与当前页面不“同源”的远端服务器的XHR。 所谓就是 URI 的协议 (scheme)、主机名 (host) 和端口号 (port) 这几部分的组合。 这被称为同源策略

Modern browsers do allow XHR requests to servers from a different origin if the server supports the CORS protocol. If the server requires user credentials, enable them in the request headers.

在现代浏览器中,如果服务器支持 CORS 协议,那么也可以向不同源的服务器发起XHR请求。 如果服务器要请求用户凭证,我们就在请求头中启用它们。

Some servers do not support CORS but do support an older, read-only alternative called JSONP. Wikipedia is one such server.

有些服务器不支持 CORS,但支持一种老的、只读的(译注:即仅支持 GET)备选协议,这就是 JSONP。 Wikipedia就是一个这样的服务器。

This Stack Overflow answer covers many details of JSONP.

这个 StackOverflow 上的答案覆盖了关于 JSONP 的很多细节。

Search Wikipedia

搜索 Wikipedia

Here is a simple search that shows suggestions from Wikipedia as the user types in a text box:

我们来构建一个简单的搜索程序,当我们在文本框中输入时,它会从 Wikipedia 中获取并显示建议的词汇列表:

Wikipedia search app (v.1)

Wikipedia offers a modern CORS API and a legacy JSONP search API. This example uses the latter. The Angular Jsonp service both extends the Http service for JSONP and restricts you to GET requests. All other HTTP methods throw an error because JSONP is a read-only facility.

Wikipedia 提供了一个现代的CORS API和一个传统的JSONP搜索 API。在这个例子中,我们使用后者。 Angular 的Jsonp服务不但通过 JSONP 扩展了Http服务,而且限制我们只能用GET请求。 尝试调用所有其它 HTTP 方法都将抛出一个错误,因为 JSONP 是只读的。

As always, wrap the interaction with an Angular data access client service inside a dedicated service, here called WikipediaService.

像往常一样,我们把和 Angular 数据访问服务进行交互的代码全都封装在一个专门的服务中。我们称之为WikipediaService


import { Injectable } from '@angular/core'; import { Jsonp, URLSearchParams } from '@angular/http'; import 'rxjs/add/operator/map'; @Injectable() export class WikipediaService { constructor(private jsonp: Jsonp) {} search (term: string) { let wikiUrl = ''; let 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(response => <string[]> response.json()[1]); } }

The constructor expects Angular to inject its Jsonp service, which is available because JsonpModule is in the root @NgModule imports array in app.module.ts.

这个构造函数期望 Angular 给它注入一个jsonp服务。 前面我们已经把JsonpModule导入到了根模块中,所以这个服务已经可以使用了。

Search parameters


The Wikipedia "opensearch" API expects four parameters (key/value pairs) to arrive in the request URL's query string. The keys are search, action, format, and callback. The value of the search key is the user-supplied search term to find in Wikipedia. The other three are the fixed values "opensearch", "json", and "JSONP_CALLBACK" respectively.

Wikipedia 的 'opensearch' API 期待在所请求的 URL 中带四个查询参数(键/值对格式)。 这些键 (key) 分别是searchactionformatcallbacksearch的值是用户提供的用于在 Wikipedia 中查找的关键字。 另外三个参数是固定值,分别是 "opensearch"、"json" 和 "JSONP_CALLBACK"。

The JSONP technique requires that you pass a callback function name to the server in the query string: callback=JSONP_CALLBACK. The server uses that name to build a JavaScript wrapper function in its response, which Angular ultimately calls to extract the data. All of this happens under the hood.

JSONP技术需要我们通过查询参数传给服务器一个回调函数的名字:callback=JSONP_CALLBACK。 服务器使用这个名字在它的响应体中构建一个 JavaScript 包装函数,Angular 最终会调用这个包装函数来提取出数据。 这些都是 Angular 在背后默默完成的,你不会感受到它。

If you're looking for articles with the word "Angular", you could construct the query string by hand and call jsonp like this:

如果我们要找那些含有关键字 “Angular” 的文档,我们可以先手工构造出查询字符串,并像这样调用jsonp

let queryString = `?search=${term}&action=opensearch&format=json&callback=JSONP_CALLBACK`; return this.jsonp .get(wikiUrl + queryString) .map(response => <string[]> response.json()[1]);

In more parameterized examples you could build the query string with the Angular URLSearchParams helper:

在更加参数化的例子中,我们会首选 Angular 的URLSearchParams辅助类来构建查询字符串,就像这样:

src/app/wiki/wikipedia.service.ts (search parameters)

let 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');

This time you call jsonp with two arguments: the wikiUrl and an options object whose search property is the params object.


src/app/wiki/wikipedia.service.ts (call jsonp)

// TODO: Add error handling return this.jsonp .get(wikiUrl, { search: params }) .map(response => <string[]> response.json()[1]);

Jsonp flattens the params object into the same query string you saw earlier, sending the request to the server.


The WikiComponent

WikiComponent 组件

Now that you have a service that can query the Wikipedia API, turn your attention to the component (template and class) that takes user input and displays search results.

现在,我们有了一个可用于查询 Wikpedia API 的服务, 我们重新回到组件中,接收用户输入,并显示搜索结果。


import { Component } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { WikipediaService } from './wikipedia.service'; @Component({ selector: 'my-wiki', template: ` <h1>Wikipedia Demo</h1> <p>Search after each keystroke</p> <input #term (keyup)="search(term.value)"/> <ul> <li *ngFor="let item of items | async">{{item}}</li> </ul>`, providers: [ WikipediaService ] }) export class WikiComponent { items: Observable<string[]>; constructor (private wikipediaService: WikipediaService) { } search (term: string) { this.items =; } }

The template presents an <input> element search box to gather search terms from the user, and calls a search(term) method after each keyup event.

该模板有一个<input>元素,它是用来从用户获取搜索关键词的搜索框。 在每次keyup事件被触发时,它调用search(term)方法。

The component's search(term) method delegates to the WikipediaService, which returns an Observable array of string results (Observable<string[]>). Instead of subscribing to the Observable inside the component, as in the HeroListComponent, the app forwards the Observable result to the template (via items) where the async pipe in the ngFor handles the subscription. Read more about async pipes in the Pipes page.

search(term)方法委托WikipediaService服务来完成实际操作。 该服务返回的是一个字符串数组的可观察对象 (Observable<string[]>)。 没有像HeroListComponent那样在组件内部订阅这个可观察对象, 我们把这个可观察对象作为结果传给模板(通过items属性), 模板中ngFor上的 async(异步)管道会对这个订阅进行处理。 关于异步管理的更多信息,见 Pipes

The async pipe is a good choice in read-only components where the component has no need to interact with the data.


HeroListComponent can't use the pipe because addHero() pushes newly created heroes into the list.


A wasteful app


The Wikipedia search makes too many calls to the server. It is inefficient and potentially expensive on mobile devices with limited data plans.

这个 Wikipedia 搜索程序触发了过多的服务器调用。 这样效率很低,而且在流量受限的移动设备上会显得过于昂贵。

1. Wait for the user to stop typing

1. 等用户停止输入

Presently, the code calls the server after every keystroke. It should only make requests when the user stops typing. Here's how it will work after refactoring:

我们目前会在每次按键之后调用服务器。 但合理的方式是只在用户停止输入之后才发起请求。 重构之后,它将这样工作:

Wikipedia search app (v.2)

2. Search when the search term changes

2. 当搜索关键字变化了才搜索

Suppose a user enters the word angular in the search box and pauses for a while. The application issues a search request for angular.

假设用户在输入框中输入了单词 angular,然后稍等片刻。 应用程序就会发出一个对 angular 的搜索请求。

Then the user backspaces over the last three letters, lar, and immediately re-types lar before pausing once more. The search term is still angular. The app shouldn't make another request.

然后,用户用退格键删除了最后三个字符 lar,并且毫不停顿的重新输入了 lar。 搜索关键词仍然是 “angular”。这时应用程序不应该发起另一个请求。

3. Cope with out-of-order responses

3. 对付乱序响应体

The user enters angular, pauses, clears the search box, and enters http. The application issues two search requests, one for angular and one for http.

用户输入了 angular,暂停,清除搜索框,然后输入 http。 应用程序发起了两个搜索请求,一个搜 angular,一个搜 http

Which response arrives first? It's unpredictable. When there are multiple requests in-flight, the app should present the responses in the original request order. In this example, the app must always display the results for the http search no matter which response arrives first.

哪一个响应会先回来?我们是没法保证的。 即使有多个尚未返回的请求,应用程序也应该按照原始请求的顺序展示对它们的响应。 如果能让 angular 的结果始终在后面返回,就不会发生这样的混乱了。

More fun with Observables

Observable 的更多乐趣

You can address these problems and improve the app with the help of some nifty observable operators.


You could make changes to the WikipediaService, but for a better user experience, create a copy of the WikiComponent instead and make it smarter, with the help of some nifty Observable operators.

我们本可以把这些改动合并进WikipediaService中,但是为了更好用户体验, 我们创建一个WikiComponent的复本,让它变得更智能。 下面是WikiSmartComponent,它使用同样的模板:

Here's the WikiSmartComponent, shown next to the original WikiComponent:


import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; import 'rxjs/add/operator/switchMap'; import { Subject } from 'rxjs/Subject'; import { WikipediaService } from './wikipedia.service'; @Component({ selector: 'my-wiki-smart', template: ` <h1>Smarter Wikipedia Demo</h1> <p>Search when typing stops</p> <input #term (keyup)="search(term.value)"/> <ul> <li *ngFor="let item of items | async">{{item}}</li> </ul>`, providers: [ WikipediaService ] }) export class WikiSmartComponent implements OnInit { items: Observable<string[]>; constructor (private wikipediaService: WikipediaService) {} private searchTermStream = new Subject<string>(); search(term: string) {; } ngOnInit() { this.items = this.searchTermStream .debounceTime(300) .distinctUntilChanged() .switchMap((term: string) =>; } } import { Component } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { WikipediaService } from './wikipedia.service'; @Component({ selector: 'my-wiki', template: ` <h1>Wikipedia Demo</h1> <p>Search after each keystroke</p> <input #term (keyup)="search(term.value)"/> <ul> <li *ngFor="let item of items | async">{{item}}</li> </ul>`, providers: [ WikipediaService ] }) export class WikiComponent { items: Observable<string[]>; constructor (private wikipediaService: WikipediaService) { } search (term: string) { this.items =; } }

While the templates are virtually identical, there's a lot more RxJS in the "smart" version, starting with debounceTime, distinctUntilChanged, and switchMap operators, imported as described above.

虽然它们的模板几乎相同,但是这个“智能”版涉及到了更多RxJS,比如debounceTimedistinctUntilChangedswitchMap操作符, 就像前面提过的那样导入。

Create a stream of search terms


The WikiComponent passes a new search term directly to the WikipediaService after every keystroke.


The WikiSmartComponent class turns the user's keystrokes into an Observable stream of search terms with the help of a Subject, which you import from RxJS:

WikiSmartComponent类借助一个Subject实例把用户的按键传给一个搜索关键词的可观察流(Observable stream)。Subject是从RxJS中导入的。

import { Subject } from 'rxjs/Subject';

The component creates a searchTermStream as a Subject of type string. The search() method adds each new search box value to that stream via the subject's next() method.


private searchTermStream = new Subject<string>(); search(term: string) {; }

Listen for search terms


The WikiSmartComponent listens to the stream of search terms and processes that stream before calling the service.


this.items = this.searchTermStream .debounceTime(300) .distinctUntilChanged() .switchMap((term: string) =>;

The role of switchMap is particularly important. The WikipediaService returns a separate Observable of string arrays (Observable<string[]>) for each search request. The user could issue multiple requests before a slow server has had time to reply, which means a backlog of response Observables could arrive at the client, at any moment, in any order.

switchMap的角色是至关重要的。 WikipediaService为每个搜索请求返回一个独立的字符串数组型的可观察对象(Observable<string[]>)。 在一个慢速服务器有时间回复之前,用户可能会发起多个请求,这意味着这个流中的响应体可能在任何时刻、以任何顺序抵达客户端。

The switchMap returns its own Observable that combines all WikipediaService response Observables, re-arranges them in their original request order, and delivers to subscribers only the most recent search results.

switchMap返回一个自有的可观察对象,该对象组合WikipediaService中的所有响应体, 并把它们按原始请求顺序排列,只把最近一次返回的搜索结果提交给订阅者。

Guarding against Cross-Site Request Forgery


In a cross-site request forgery (CSRF or XSRF), an attacker tricks the user into visiting a different web page with malignant code that secretly sends a malicious request to your application's web server.

在一个跨站请求伪造攻击(CSRF 或 XSRF)中,攻击者欺骗用户访问一个不同的网页,它带有恶意代码,秘密向你的应用程序服务器发送恶意请求。

The server and client application must work together to thwart this attack. Angular's Http client does its part by applying a default CookieXSRFStrategy automatically to all requests.

客户端和服务器必须合作来抵挡这种攻击。 Angular 的http客户端自动使用它默认的CookieXSRFStrategy来完成客户端的任务。

The CookieXSRFStrategy supports a common anti-XSRF technique in which the server sends a randomly generated authentication token in a cookie named XSRF-TOKEN. The HTTP client adds an X-XSRF-TOKEN header with that token value to subsequent requests. The server receives both the cookie and the header, compares them, and processes the request only if the cookie and header match.

CookieXSRFStrategy支持常见的反 XSRF 技术,服务端发送一个随机生成的认证令牌到名为XSRF-TOKEN的 cookie 中。 HTTP 客户端使用该令牌的值为所有后续请求添加一个X-XSRF-TOKEN页头。 服务器接受这个 cookie 和页头,比较它们,只有在它们匹配的时候才处理请求。

See the XSRF topic on the Security page for more information about XSRF and Angular's XSRFStrategy counter measures.


Override default request headers (and other request options)


Request options (such as headers) are merged into the default RequestOptions before the request is processed. The HttpModule provides these default options via the RequestOptions token.

请求选项(比如请求头),会在发起请求之前并入default RequestOptions中。 HttpModule通过RequestOptions令牌提供了这些默认选项。

You can override these defaults to suit your application needs by creating a custom sub-class of RequestOptions that sets the default options for the application.


This sample creates a class that sets the default Content-Type header to JSON. It exports a constant with the necessary RequestOptions provider to simplify registration in AppModule.

这个例子创建了一个类,它把默认的Content-Type请求头设置为JSON。 它导出了一个带有RequestOptions提供商的常量,以便注册进AppModule中。


import { Injectable } from '@angular/core'; import { BaseRequestOptions, RequestOptions } from '@angular/http'; @Injectable() export class DefaultRequestOptions extends BaseRequestOptions { constructor() { super(); // Set the default 'Content-Type' header this.headers.set('Content-Type', 'application/json'); } } export const requestOptionsProvider = { provide: RequestOptions, useClass: DefaultRequestOptions };

Then it registers the provider in the root AppModule.


src/app/app.module.ts (provide default request header)

providers: [ requestOptionsProvider ],

Remember to include this provider during setup when unit testing the app's HTTP services.


After this change, the header option setting in HeroService.create() is no longer necessary,


src/app/toh/hero.service.ts (create)

create(name: string): Observable<Hero> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); return, { name }, options) .map(this.extractData) .catch(this.handleError); }

You can confirm that DefaultRequestOptions is working by examing HTTP requests in the browser developer tools' network tab. If you're short-circuiting the server call with something like the in-memory web api, try commenting-out the create header option, set a breakpoint on the POST call, and step through the request processing to verify the header is there.

打开浏览器开发者工具的network页,我们就可以确认,DefaultRequestOptions工作是否正常。 如果你通过某些机制(比如内存Web API)短路了对服务器的调用, 可以试试注释掉create头选项,在POST调用上设置断点,并单步跟踪进请求过程来验证是否添加了这个头。

Individual requests options, like this one, take precedence over the default RequestOptions. It might be wise to keep the create request header setting for extra safety.


Appendix: Tour of Heroes in-memory web api

附录:《英雄指南》的内存 (in-memory) 服务器

If the app only needed to retrieve data, you could get the heroes from a heroes.json file:

如果我们只关心获取到的数据,我们可以告诉 Angular 从一个heroes.json文件中获取英雄列表,就像这样:


{ "data": [ { "id": 1, "name": "Windstorm" }, { "id": 2, "name": "Bombasto" }, { "id": 3, "name": "Magneta" }, { "id": 4, "name": "Tornado" } ] }

You wrap the heroes array in an object with a data property for the same reason that a data server does: to mitigate the security risk posed by top-level JSON arrays.

我们把英雄数组包装进一个带data属性的对象中,就像一个真正的数据服务器所应该做的那样。 这样可以缓解由顶级 JSON 数组导致的安全风险

You'd set the endpoint to the JSON file like this:

我们要像这样把端点设置为这个 JSON 文件:


private heroesUrl = 'app/heroes.json'; // URL to JSON file

The get heroes scenario would work, but since the app can't save changes to a JSON file, it needs a web API server. Because there isn't a real server for this demo, it substitutes the Angular in-memory web api simulator for the actual XHR backend service.

这在获取英雄数据的场景下确实能工作, 但我们不能把这些改动保存到 JSON 文件中,因此需要一个 Web API 服务器。 因为这个演示程序中并没有一个真实的服务器, 所以,我们使用内存 Web API 仿真器代替它。

The in-memory web api is not part of Angular proper. It's an optional service in its own angular-in-memory-web-api library installed with npm (see package.json).

内存Web API并不是Angular本身的一部分。 它是一个可选服务,要用npm来单独安装angular-in-memory-web-api库(参见package.json)。

See the README file for configuration options, default behaviors, and limitations.

参见README file来了解配置选项、默认行为和限制。

The in-memory web API gets its data from a custom application class with a createDb() method that returns a map whose keys are collection names and whose values are arrays of objects in those collections.

内存 Web API 从一个带有createDb()方法的自定义类中获取数据,并且返回一个 map,它的主键 (key) 是一组名字,而值 (value) 是一组与之对应的对象数组。

Here's the class for this sample, based on the JSON data:

这里是与范例中基于 JSON 的数据源完成相同功能的类:


import { InMemoryDbService } from 'angular-in-memory-web-api'; export class HeroData implements InMemoryDbService { createDb() { let heroes = [ { id: 1, name: 'Windstorm' }, { id: 2, name: 'Bombasto' }, { id: 3, name: 'Magneta' }, { id: 4, name: 'Tornado' } ]; return {heroes}; } }

Ensure that the HeroService endpoint refers to the web API:

确保HeroService的端点指向了这个 Web API:


private heroesUrl = 'api/heroes'; // URL to web API

Finally, redirect client HTTP requests to the in-memory web API by adding the InMemoryWebApiModule to the AppModule.imports list. At the same time, call its forRoot() configuration method with the HeroData class.

使用内存 Web API 服务模块很容易配置重定向,将InMemoryWebApiModule添加到AppModule.imports列表中, 同时在HeroData类中调用forRoot()配置方法。



How it works


Angular's http service delegates the client/server communication tasks to a helper service called the XHRBackend.

这次重定向非常容易配置,这是因为 Angular 的http服务把客户端/服务器通讯的工作委托给了一个叫做XHRBackend的辅助服务。

Using standard Angular provider registration techniques, the InMemoryWebApiModule replaces the default XHRBackend service with its own in-memory alternative. At the same time, the forRoot method initializes the in-memory web API with the seed data from the mock hero dataset.

使用标准 Angular 提供商注册方法,InMemoryWebApiModule替代默认的XHRBackend服务并使用它自己的内存存储服务。 forRoot方法来自模拟的英雄数据集的种子数据初始化了这个内存 Web API。

The forRoot() method name is a strong reminder that you should only call the InMemoryWebApiModule once, while setting the metadata for the root AppModule. Don't call it again.


Here is the final, revised version of src/app/app.module.ts, demonstrating these steps.


src/app/app.module.ts (excerpt)

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { HttpModule, JsonpModule } from '@angular/http'; import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; import { HeroData } from './hero-data'; import { requestOptionsProvider } from './default-request-options.service'; import { AppComponent } from './app.component'; import { HeroListComponent } from './toh/hero-list.component'; import { HeroListPromiseComponent } from './toh/hero-list.component.promise'; import { WikiComponent } from './wiki/wiki.component'; import { WikiSmartComponent } from './wiki/wiki-smart.component'; @NgModule({ imports: [ BrowserModule, FormsModule, HttpModule, JsonpModule, InMemoryWebApiModule.forRoot(HeroData) ], declarations: [ AppComponent, HeroListComponent, HeroListPromiseComponent, WikiComponent, WikiSmartComponent ], providers: [ requestOptionsProvider ], bootstrap: [ AppComponent ] }) export class AppModule {}

Import the InMemoryWebApiModule after the HttpModule to ensure that the XHRBackend provider of the InMemoryWebApiModule supersedes all others.


See the full source code in the .