The Tour of Heroes is evolving and we anticipate adding more components in the near future.


Multiple components will need access to hero data and we don't want to copy and paste the same code over and over. Instead, we'll create a single reusable data service and learn to inject it in the components that need it.

将来会有更多的组件访问英雄数据,我们不想一遍一遍地复制粘贴同样的代码。 解决方案是,创建一个单一的、可复用的数据服务,然后学着把它注入到那些需要它的组件中去。

Refactoring data access to a separate service keeps the component lean and focused on supporting the view. It also makes it easier to unit test the component with a mock service.

我们将重构数据访问代码,把它隔离到一个独立的服务中去,让组件尽可能保持精简,专注于为视图提供支持。 在这种方式下,借助模拟服务来对组件进行单元测试也会更容易。

Because data services are invariably asynchronous, we'll finish the chapter with a Promise-based version of the data service.

因为数据服务通常都是异步的,我们将在本章创建一个基于承诺 (Promise)的数据服务。

Run the for this part.


Where We Left Off


Before we continue with our Tour of Heroes, let’s verify we have the following structure. If not, we’ll need to go back and follow the previous chapters.


node_modules ...

Keep the app transpiling and running


Open a terminal/console window. Start the TypeScript compiler, watch for changes, and start our server by entering the command:

打开终端/控制台窗口,启动 TypeScript 编译器,它会监视文件变更,并启动开发服务器。只要敲:

npm start

The application runs and updates automatically as we continue to build the Tour of Heroes.


Creating a Hero Service


Our stakeholders have shared their larger vision for our app. They tell us they want to show the heroes in various ways on different pages. We already can select a hero from a list. Soon we'll add a dashboard with the top performing heroes and create a separate view for editing hero details. All three views need hero data.

客户向我们描绘了本应用更大的目标:想要在不同的页面中用多种方式显示英雄。 现在我们已经能从列表中选择一个英雄了,但这还不够。 很快,我们将添加一个仪表盘来显示表现最好的英雄,并创建一个独立视图来编辑英雄的详情。 所有这些视图都需要英雄数据。

At the moment the AppComponent defines mock heroes for display. We have at least two objections. First, defining heroes is not the component's job. Second, we can't easily share that list of heroes with other components and views.

目前,AppComponent显示的是模拟数据。 至少有两个地方可以改进: 首先,定义英雄的数据不该是组件的任务; 其次,想把这份英雄列表的数据共享给其它组件和视图可不那么容易。

We can refactor this hero data acquisition business to a single service that provides heroes, and share that service with all components that need heroes.


Create the HeroService

创建 HeroService

Create a file in the app folder called hero.service.ts.


We've adopted a convention in which we spell the name of a service in lowercase followed by .service. If the service name were multi-word, we'd spell the base filename in lower dash-case. The SpecialSuperHeroService would be defined in the special-super-hero.service.ts file.

我们遵循的文件命名约定是:服务名称的小写形式(基本名),加上.service后缀。 如果服务名称包含多个单词,我们就把基本名部分写成中线形式 (dash-case)。 例如,SpecialSuperHeroService服务应该被定义在special-super-hero.service.ts文件中。

We name the class HeroService and export it for others to import.


src/app/hero.service.ts (starting point)

import { Injectable } from '@angular/core'; @Injectable() export class HeroService { }

Injectable Services


Notice that we imported the Angular Injectable function and applied that function as an @Injectable() decorator.

注意,我们导入了 Angular 的Injectable函数,并作为@Injectable()装饰器使用这个函数。

Don't forget the parentheses! Neglecting them leads to an error that's difficult to diagnose.


TypeScript sees the @Injectable() decorator and emits metadata about our service, metadata that Angular may need to inject other dependencies into this service.

当 TypeScript 看到@Injectable()装饰器时,就会记下本服务的元数据。 如果 Angular 需要往这个服务中注入其它依赖,就会使用这些元数据。

The HeroService doesn't have any dependencies at the moment. Add the decorator anyway. It is a "best practice" to apply the @Injectable() decorator ​from the start​ both for consistency and for future-proofing.

此时HeroService还没有任何依赖,但我们还是得加上这个装饰器。 作为一项最佳实践,无论是出于提高统一性还是减少变更的目的, 都应该从一开始就加上@Injectable()装饰器。

Getting Heroes


Add a getHeroes method stub.


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

@Injectable() export class HeroService { getHeroes(): void {} // stub }

We're holding back on the implementation for a moment to make an important point.


The consumer of our service doesn't know how the service gets the data. Our HeroService could get Hero data from anywhere. It could get the data from a web service or local storage or from a mock data source.

数据使用者并不知道本服务会如何获取数据。 我们的HeroService服务可以从任何地方获取英雄的数据。 它可以从网络服务器获取,可以从浏览器的局部存储区获取,也可以从模拟的数据源。

That's the beauty of removing data access from the component. We can change our minds about the implementation as often as we like, for whatever reason, without touching any of the components that need heroes.

这就是从组件中移除数据访问代码的美妙之处。 这样我们可以随时改变数据访问的实现方式,而无需对使用英雄的组件作任何改动。

Mock Heroes


We already have mock Hero data sitting in the AppComponent. It doesn't belong there. It doesn't belong here either. We'll move the mock data to its own file.

我们曾在AppComponent组件中写过模拟数据。它不该在那里,但也不该在这里! 我们应把模拟数据移到它自己的文件中去。

Cut the HEROES array from app.component.ts and paste it to a new file in the app folder named mock-heroes.ts. We copy the import {Hero} ... statement as well because the heroes array uses the Hero class.

app.component.ts文件中剪切HEROS数组,把它粘贴到app目录下一个名叫mock-heroes.ts的文件中。 还要复制import {Hero}...语句,因为我们的英雄数组用到了Hero类。


import { Hero } from './hero'; export const HEROES: Hero[] = [ {id: 11, name: 'Mr. 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'} ];

We export the HEROES constant so we can import it elsewhere — such as our HeroService.

我们导出了HEROES常量,以便可以在其它地方导入它 — 例如HeroService服务。

Meanwhile, back in app.component.ts where we cut away the HEROES array, we leave behind an uninitialized heroes property:


src/app/app.component.ts (heroes property)

heroes: Hero[];

Return Mocked Heroes


Back in the HeroService we import the mock HEROES and return it from the getHeroes method. Our HeroService looks like this:

回到HeroService,我们导入HEROES常量,并在getHeroes方法中返回它。 我们的HeroService服务现在看起来是这样:


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

Use the Hero Service

使用 HeroService 服务

We're ready to use the HeroService in other components starting with our AppComponent.

我们可以在多个组件中使用 HeroService 服务了,先从 AppComponent 开始。

We begin, as usual, by importing the thing we want to use, the HeroService.


import { HeroService } from './hero.service';

Importing the service allows us to reference it in our code. How should the AppComponent acquire a runtime concrete HeroService instance?

导入这个服务让我们可以在代码中引用它。 AppComponent该如何在运行中获得一个具体的HeroService实例呢?

Do we new the HeroService? No way!

我们要自己 new 出这个 HeroService 吗?不!

We could create a new instance of the HeroService with new like this:


heroService = new HeroService(); // don't do this

That's a bad idea for several reasons including


We get it. Really we do. But it is so ridiculously easy to avoid these problems that there is no excuse for doing it wrong.


Inject the HeroService

注入 HeroService

Two lines replace the one line that created with new:


  1. We add a constructor that also defines a private property.


  2. We add to the component's providers metadata.


Here's the constructor:


src/app/app.component.ts (constructor)

constructor(private heroService: HeroService) { }

The constructor itself does nothing. The parameter simultaneously defines a private heroService property and identifies it as a HeroService injection site.


Now Angular will know to supply an instance of the HeroService when it creates a new AppComponent.

现在,当创建AppComponent实例时,Angular 知道需要先提供一个HeroService的实例。

Learn more about Dependency Injection in the Dependency Injection chapter.


The injector does not know yet how to create a HeroService. If we ran our code now, Angular would fail with an error:

注入器还不知道该如何创建HeroService。 如果现在运行我们的代码,Angular 就会失败,并报错:

EXCEPTION: No provider for HeroService! (AppComponent -> HeroService) (异常:没有 HeroService 的提供商!(AppComponent -> HeroService))

We have to teach the injector how to make a HeroService by registering a HeroService provider. Do that by adding the following providers array property to the bottom of the component metadata in the @Component call.

我们还得注册一个HeroService提供商,来告诉注入器如何创建HeroService。 要做到这一点,我们在@Component组件的元数据底部添加providers数组属性如下:

providers: [HeroService]

The providers array tells Angular to create a fresh instance of the HeroService when it creates a new AppComponent. The AppComponent can use that service to get heroes and so can every child component of its component tree.

providers数组告诉 Angular,当它创建新的AppComponent组件时,也要创建一个HeroService的新实例。 AppComponent会使用那个服务来获取英雄列表,在它组件树中的每一个子组件也同样如此。

getHeroes in the AppComponent

AppComponent 中的 getHeroes

We've got the service in a heroService private variable. Let's use it.


We pause to think. We can call the service and get the data in one line.


this.heroes = this.heroService.getHeroes();

We don't really need a dedicated method to wrap one line. We write it anyway:


getHeroes(): void { this.heroes = this.heroService.getHeroes(); }

The ngOnInit Lifecycle Hook

ngOnInit 生命周期钩子

AppComponent should fetch and display heroes without a fuss. Where do we call the getHeroes method? In a constructor? We do not!

毫无疑问,AppComponent应该获取英雄数据并显示它。 我们该在哪里调用getHeroes方法呢?在构造函数中吗?

Years of experience and bitter tears have taught us to keep complex logic out of the constructor, especially anything that might call a server as a data access method is sure to do.

多年的经验和惨痛的教训教育我们,应该把复杂的逻辑扔到构造函数外面去, 特别是那些需要从服务器获取数据的逻辑更是如此。

The constructor is for simple initializations like wiring constructor parameters to properties. It's not for heavy lifting. We should be able to create a component in a test and not worry that it might do real work — like calling a server! — before we tell it to do so.

构造函数是为了简单的初始化工作而设计的,例如把构造函数的参数赋值给属性。 它的负担不应该过于沉重。我们应该能在测试中创建一个组件,而不用担心它会做实际的工作 — 例如和服务器通讯,直到我们主动要求它做这些。

If not the constructor, something has to call getHeroes.


Angular will call it if we implement the Angular ngOnInit Lifecycle Hook. Angular offers a number of interfaces for tapping into critical moments in the component lifecycle: at creation, after each change, and at its eventual destruction.

这也不难。只要我们实现了 Angular 的 ngOnInit 生命周期钩子,Angular 就会主动调用这个钩子。 Angular提供了一些接口,用来介入组件生命周期的几个关键时间点:刚创建时、每次变化时,以及最终被销毁时。

Each interface has a single method. When the component implements that method, Angular calls it at the appropriate time.

每个接口都有唯一的一个方法。只要组件实现了这个方法,Angular 就会在合适的时机调用它。

Learn more about lifecycle hooks in the Lifecycle Hooks chapter.


Here's the essential outline for the OnInit interface:


src/app/app.component.ts (ngOnInit stub)

import { OnInit } from '@angular/core'; export class AppComponent implements OnInit { ngOnInit(): void { } }

We write an ngOnInit method with our initialization logic inside and leave it to Angular to call it at the right time. In our case, we initialize by calling getHeroes.

我们写了一个带有初始化逻辑的ngOnInit方法,Angular会在适当的时候调用它。 在这个例子中,我们通过调用getHeroes来完成初始化。

ngOnInit(): void { this.getHeroes(); }

Our application should be running as expected, showing a list of heroes and a hero detail view when we click on a hero name.


We're getting closer. But something isn't quite right.


Async Services and Promises


Our HeroService returns a list of mock heroes immediately. Its getHeroes signature is synchronous


this.heroes = this.heroService.getHeroes();

Ask for heroes and they are there in the returned result.


Someday we're going to get heroes from a remote server. We don’t call http yet, but we aspire to in later chapters.

将来,我们打算从远端服务器上获取英雄数据。我们还没调用 http,但在后面的章节中我们会希望这么做。

When we do, we'll have to wait for the server to respond and we won't be able to block the UI while we wait, even if we want to (which we shouldn't) because the browser won't block.

那时候,我们不得不等待服务器响应,并且在等待过程中我们无法阻塞用户界面响应, 即使我们想这么做(也不应这么做)也做不到,因为浏览器不会阻塞。

We'll have to use some kind of asynchronous technique and that will change the signature of our getHeroes method.


We'll use Promises.

我们将使用 承诺

The Hero Service makes a Promise


A Promise is ... well it's a promise to call us back later when the results are ready. We ask an asynchronous service to do some work and give it a callback function. It does that work (somewhere) and eventually it calls our function with the results of the work or an error.

承诺 就是 …… 好吧,它就是一个承诺,在有了结果时,它承诺会回调我们。 我们请求一个异步服务去做点什么,并且给它一个回调函数。 它会去做(在某个地方),一旦完成,它就会调用我们的回调函数,并通过参数把工作结果或者错误信息传给我们。

We are simplifying. Learn about ES2015 Promises here and elsewhere on the web.

这里只是粗浅的说说,要了解更多 ES2015 Promise 的信息,见这里或在 Web 上搜索其它学习资源。

Update the HeroService with this Promise-returning getHeroes method:


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

getHeroes(): Promise<Hero[]> { return Promise.resolve(HEROES); }

We're still mocking the data. We're simulating the behavior of an ultra-fast, zero-latency server, by returning an immediately resolved Promise with our mock heroes as the result.

我们继续使用模拟数据。我们通过返回一个 立即解决的承诺 的方式,模拟了一个超快、零延迟的超级服务器。

Act on the Promise


Returning to the AppComponent and its getHeroes method, we see that it still looks like this:


src/app/app.component.ts (getHeroes - old)

getHeroes(): void { this.heroes = this.heroService.getHeroes(); }

As a result of our change to HeroService, we're now setting this.heroes to a Promise rather than an array of heroes.


We have to change our implementation to act on the Promise when it resolves. When the Promise resolves successfully, then we will have heroes to display.

我们得修改这个实现,把它变成基于承诺的,并在承诺的事情被解决时再行动。 一旦承诺的事情被成功解决,我们就会显示英雄数据。

We pass our callback function as an argument to the Promise's then method:


src/app/app.component.ts (getHeroes - revised)

getHeroes(): void { this.heroService.getHeroes().then(heroes => this.heroes = heroes); }

The ES2015 arrow function in the callback is more succinct than the equivalent function expression and gracefully handles this.

回调中所用的 ES2015 箭头函数 比等价的函数表达式更加简洁,能优雅的处理 this 指针。

Our callback sets the component's heroes property to the array of heroes returned by the service. That's all there is to it!


Our app should still be running, still showing a list of heroes, and still responding to a name selection with a detail view.


Checkout the "Take it slow" appendix to see what the app might be like with a poor connection.


Review the App Structure


Let’s verify that we have the following structure after all of our good refactoring in this chapter:


node_modules ...

Here are the code files we discussed in this chapter.


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); } } import { Component, OnInit } from '@angular/core'; import { Hero } from './hero'; import { HeroService } from './hero.service'; @Component({ selector: 'my-app', template: ` <h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero === selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> <my-hero-detail [hero]="selectedHero"></my-hero-detail> `, styles: [` .selected { background-color: #CFD8DC !important; color: white; } .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; } .heroes li { cursor: pointer; position: relative; left: 0; background-color: #EEE; margin: .5em; padding: .3em 0; height: 1.6em; border-radius: 4px; } .heroes li.selected:hover { background-color: #BBD8DC !important; color: white; } .heroes li:hover { color: #607D8B; background-color: #DDD; left: .1em; } .heroes .text { position: relative; top: -3px; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; background-color: #607D8B; line-height: 1em; position: relative; left: -1px; top: -4px; height: 1.8em; margin-right: .8em; border-radius: 4px 0 0 4px; } `], providers: [HeroService] }) export class AppComponent implements OnInit { title = 'Tour of Heroes'; heroes: Hero[]; selectedHero: Hero; constructor(private heroService: HeroService) { } getHeroes(): void { this.heroService.getHeroes().then(heroes => this.heroes = heroes); } ngOnInit(): void { this.getHeroes(); } onSelect(hero: Hero): void { this.selectedHero = hero; } } import { Hero } from './hero'; export const HEROES: Hero[] = [ {id: 11, name: 'Mr. 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'} ];

The Road We’ve Travelled


Let’s take stock of what we’ve built.


Run the for this part.


The Road Ahead


Our Tour of Heroes has become more reusable using shared components and services. We want to create a dashboard, add menu links that route between the views, and format data in a template. As our app evolves, we’ll learn how to design it to make it easier to grow and maintain.

通过使用共享组件和服务,我们的《英雄指南》更有复用性了。 我们还要创建一个仪表盘,要添加在视图间路由的菜单链接,还要在模板中格式化数据。 随着我们应用的进化,我们还会学到如何进行设计,让它更易于扩展和维护。

We learn about Angular Component Router and navigation among the views in the next tutorial chapter.

我们将在下一章学习 Angular 组件路由,以及在视图间导航的知识。

Appendix: Take it slow


We can simulate a slow connection.


Import the Hero symbol and add the following getHeroesSlowly method to the HeroService


src/app/hero.service.ts (getHeroesSlowly)

getHeroesSlowly(): Promise<Hero[]> { return new Promise(resolve => { // Simulate server latency with 2 second delay setTimeout(() => resolve(this.getHeroes()), 2000); }); }

Like getHeroes, it also returns a Promise. But this Promise waits 2 seconds before resolving the Promise with mock heroes.

getHeroes一样,它也返回一个承诺。 但是,这个承诺会在提供模拟数据之前等待两秒钟。

Back in the AppComponent, replace heroService.getHeroes with heroService.getHeroesSlowly and see how the app behaves.