英雄指南的HeroesComponent目前获取和显示的都是模拟数据。
本节课的重构完成之后,HeroesComponent变得更精简,并且聚焦于为它的视图提供支持。这也让它更容易使用模拟服务进行单元测试。
如果你希望从 GitHub 上查看我们提供测试的源代码,你可以访问下面的链接:https://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services
为什么需要服务
组件不应该直接获取或保存数据,它们不应该了解是否在展示假数据。 它们应该聚焦于展示数据,而把数据访问的职责委托给某个服务。
本节课,你将创建一个HeroService,应用中的所有类都可以使用它来获取英雄列表。 不要使用new来创建此服务,而要依靠 Angular 的依赖注入机制把它注入到HeroesComponent的构造函数中。
服务是在多个“互相不知道”的类之间共享信息的好办法。 你将创建一个MessageService,并且把它注入到两个地方:
HeroService中,它会使用该服务发送消息。
MessagesComponent中,它会显示其中的消息。
创建HeroService
使用 Angular CLI 创建一个名叫hero的服务。
ng generate service hero
该命令会在src/app/hero.service.ts中生成HeroService类的骨架。HeroService类的代码如下:
src/app/hero.service.ts (new service)
import{ Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export classHeroService {
constructor() { }
}
@Injectable()服务
注意,这个新的服务导入了 Angular 的Injectable符号,并且给这个服务类添加了@Injectable()装饰器。 它把这个类标记为依赖注入系统的参与者之一。HeroService类将会提供一个可注入的服务,并且它还可以拥有自己的待注入的依赖。 目前它还没有依赖,但是很快就会有了。
@Injectable()装饰器会接受该服务的元数据对象,就像@Component()对组件类的作用一样。
获取英雄数据
HeroService可以从任何地方获取数据:Web 服务、本地存储(LocalStorage)或一个模拟的数据源。
从组件中移除数据访问逻辑,意味着将来任何时候你都可以改变目前的实现方式,而不用改动任何组件。 这些组件不需要了解该服务的内部实现。
这节课中的实现仍然会提供模拟的英雄列表。
导入Hero和HEROES。
import{ Hero } from './hero';
import{ HEROES } from './mock-heroes';
添加一个getHeroes方法,让它返回模拟的英雄列表。
getHeroes(): Hero[] {
returnHEROES;
}
提供(provide)HeroService
在要求 Angular 把HeroService注入到HeroesComponent之前,你必须先把这个服务提供给依赖注入系统。稍后你就要这么做。 你可以通过注册提供商来做到这一点。提供商用来创建和交付服务,在这个例子中,它会对HeroService类进行实例化,以提供该服务。
现在,你需要确保HeroService已经作为该服务的提供商进行过注册。 你要用一个注入器注册它。注入器就是一个对象,负责在需要时选取和注入该提供商。
默认情况下,Angular CLI 命令ng generate service会通过给@Injectable装饰器添加元数据的形式,用根注入器将你的服务注册成为提供商。
如果你看看HeroService紧前面的@Injectable()语句定义,就会发现providedIn元数据的值是 'root':
@Injectable({
providedIn: 'root',
})
@
Injectable
({ providedIn:'root',})
当你在顶层提供该服务时,Angular 就会为HeroService创建一个单一的、共享的实例,并把它注入到任何想要它的类上。 在@Injectable元数据中注册该提供商,还能允许 Angular 通过移除那些完全没有用过的服务来进行优化。
要了解关于提供商的更多知识,参见提供商部分。 要了解关于注入器的更多知识,参见依赖注入指南。
现在HeroService已经准备好插入到HeroesComponent中了。
这是一个过渡性的代码范例,它将会允许你提供并使用HeroService。此刻的代码和最终代码相差很大。
修改HeroesComponent
打开HeroesComponent类文件。
删除HEROES的导入语句,因为你以后不会再用它了。 转而导入HeroService。
src/app/heroes/heroes.component.ts (import HeroService)
import{ HeroService } from '../hero.service';
把heroes属性的定义改为一句简单的声明。
heroes: Hero[];
注入HeroService
往构造函数中添加一个私有的heroService,其类型为HeroService。
constructor(privateheroService: HeroService) { }
这个参数同时做了两件事:1. 声明了一个私有heroService属性,2. 把它标记为一个HeroService的注入点。
当 Angular 创建HeroesComponent时,依赖注入系统就会把这个heroService参数设置为HeroService的单例对象。
添加getHeroes()
创建一个函数,以从服务中获取这些英雄数据。
getHeroes(): void{
this.heroes = this.heroService.getHeroes();
}
在ngOnInit中调用它
你固然可以在构造函数中调用getHeroes(),但那不是最佳实践。
让构造函数保持简单,只做初始化操作,比如把构造函数的参数赋值给属性。 构造函数不应该做任何事。 它当然不应该调用某个函数来向远端服务(比如真实的数据服务)发起 HTTP 请求。
而是选择在 ngOnInit 生命周期钩子中调用 getHeroes(),之后交由 Angular 处理,它会在构造出 HeroesComponent 的实例之后的某个合适的时机调用 ngOnInit。
ngOnInit() {
this.getHeroes();
}
查看运行效果
刷新浏览器,该应用仍运行的一如既往。 显示英雄列表,并且当你点击某个英雄的名字时显示出英雄详情视图。
可观察(Observable)的数据
HeroService.getHeroes()的函数签名是同步的,它所隐含的假设是HeroService总是能同步获取英雄列表数据。 而HeroesComponent也同样假设能同步取到getHeroes()的结果。
this.heroes = this.heroService.getHeroes();
这在真实的应用中几乎是不可能的。 现在能这么做,只是因为目前该服务返回的是模拟数据。 不过很快,该应用就要从远端服务器获取英雄数据了,而那天生就是异步操作。
HeroService必须等服务器给出响应, 而getHeroes()不能立即返回英雄数据, 浏览器也不会在该服务等待期间停止响应。
HeroService.getHeroes()必须具有某种形式的异步函数签名。
它可以使用回调函数,可以返回Promise(承诺),也可以返回Observable(可观察对象)。
这节课,HeroService.getHeroes()将会返回Observable,因为它最终会使用 Angular 的HttpClient.get方法来获取英雄数据,而HttpClient.get()会返回Observable。
可观察对象版本的HeroService
Observable是RxJS 库中的一个关键类。
在稍后的 HTTP 教程中,你就会知道 AngularHttpClient的方法会返回 RxJS 的Observable。 这节课,你将使用 RxJS 的of()函数来模拟从服务器返回数据。
打开HeroService文件,并从 RxJS 中导入Observable和of符号。
src/app/hero.service.ts (Observable imports)
import{ Observable, of } from 'rxjs';
把getHeroes方法改成这样:
getHeroes(): Observable
returnof(HEROES);
}
of(HEROES)会返回一个Observable
在HTTP 教程中,你将会调用HttpClient.get
在HeroesComponent中订阅
HeroService.getHeroes方法之前返回一个Hero[], 现在它返回的是Observable
你必须在HeroesComponent中也向本服务中的这种形式看齐。
找到getHeroes方法,并且把它替换为如下代码(和前一个版本对比显示):
heroes.component.ts (Observable)
getHeroes(): void{
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
heroes.component.ts (Original)
getHeroes(): void{
this.heroes = this.heroService.getHeroes();
}
Observable.subscribe()是关键的差异点。
上一个版本把英雄的数组赋值给了该组件的heroes属性。 这种赋值是同步的,这里包含的假设是服务器能立即返回英雄数组或者浏览器能在等待服务器响应时冻结界面。
当HeroService真的向远端服务器发起请求时,这种方式就行不通了。
新的版本等待Observable发出这个英雄数组,这可能立即发生,也可能会在几分钟之后。 然后,subscribe函数把这个英雄数组传给这个回调函数,该函数把英雄数组赋值给组件的heroes属性。
使用这种异步方式,当HeroService从远端服务器获取英雄数据时,就可以工作了。
显示消息
在这一节,你将
添加一个MessagesComponent,它在屏幕的底部显示应用中的消息。
创建一个可注入的、全应用级别的MessageService,用于发送要显示的消息。
把MessageService注入到HeroService中。
当HeroService成功获取了英雄数据时显示一条消息。
创建MessagesComponent
使用 CLI 创建MessagesComponent。
ng generate component messages
CLI 在src/app/messages中创建了组件文件,并且把MessagesComponent声明在了AppModule中。
修改AppComponent的模板来显示所生成的MessagesComponent:
/src/app/app.component.html
{{title}}
你可以在页面的底部看到来自的MessagesComponent的默认内容。
创建MessageService
使用 CLI 在src/app中创建MessageService。
ng generate service message
打开MessageService,并把它的内容改成这样:
/src/app/message.service.ts
import{ Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export classMessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}
该服务对外暴露了它的messages缓存,以及两个方法:add()方法往缓存中添加一条消息,clear()方法用于清空缓存。
把它注入到HeroService中
重新打开HeroService,并且导入MessageService。
/src/app/hero.service.ts (import MessageService)
import{ MessageService } from './message.service';
修改这个构造函数,添加一个私有的messageService属性参数。 Angular 将会在创建HeroService时把MessageService的单例注入到这个属性中。
constructor(privatemessageService: MessageService) { }
这是一个典型的“服务中的服务”场景: 你把MessageService注入到了HeroService中,而HeroService又被注入到了HeroesComponent中。
从HeroService中发送一条消息
修改getHeroes方法,在获取到英雄数组时发送一条消息。
getHeroes(): Observable
// TODO: send the message _after_ fetching the heroes
this.messageService.add('HeroService: fetched heroes');
returnof(HEROES);
}
从HeroService中显示消息
MessagesComponent可以显示所有消息, 包括当HeroService获取到英雄数据时发送的那条。
打开MessagesComponent,并且导入MessageService。
/src/app/messages/messages.component.ts (import MessageService)
import{ MessageService } from '../message.service';
修改构造函数,添加一个public的messageService属性。 Angular 将会在创建MessagesComponent的实例时 把MessageService的实例注入到这个属性中。
constructor(publicmessageService: MessageService) {}
这个messageService属性必须是公共属性,因为你将会在模板中绑定到它。
Angular 只会绑定到组件的公共属性。
绑定到MessageService
把 CLI 生成的MessagesComponent的模板改成这样:
src/app/messages/messages.component.html
Messages
这个模板直接绑定到了组件的messageService属性上。
*ngIf只有在有消息时才会显示消息区。
*ngFor用来在一系列
Angular 的事件绑定把按钮的click事件绑定到了MessageService.clear()。
当你把最终代码某一页的内容添加到messages.component.css中时,这些消息会变得好看一些。
刷新浏览器,页面显示出了英雄列表。 滚动到底部,就会在消息区看到来自HeroService的消息。 点击“清空”按钮,消息区不见了。
查看最终代码
你的应用应该变成了这样在线例子/下载范例。本页所提及的代码文件如下。
如果你想直接在 stackblitz 运行本页中的例子,请单击链接:https://stackblitz.com/github/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services
本页中所提及的代码如下:https://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services
对应的文件列表和代码链接如下:
文件名源代码
src/app/hero.service.tshttps://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services/blob/master/src/app/hero.service.ts
src/app/heroes/heroes.component.tshttps://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services/blob/master/src/app/heroes/heroes.component.ts
src/app/messages/messages.component.tshttps://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services/blob/master/src/app/messages/messages.component.ts
src/app/messages/messages.component.htmlhttps://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services/blob/master/src/app/messages/messages.component.html
src/app/messages/messages.component.csshttps://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services/blob/master/src/app/messages/messages.component.css
src/app/app.module.tshttps://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services/blob/master/src/app/app.module.ts
src/app/app.component.htmlhttps://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services/blob/master/src/app/app.component.html
小结
你把数据访问逻辑重构到了HeroService类中。
你在根注入器中把HeroService注册为该服务的提供商,以便在别处可以注入它。
你使用Angular 依赖注入机制把它注入到了组件中。
你给HeroService中获取数据的方法提供了一个异步的函数签名。
你发现了Observable以及 RxJS 库。
你使用 RxJS 的of()方法返回了一个模拟英雄数据的可观察对象(Observable
在组件的ngOnInit生命周期钩子中调用HeroService方法,而不是构造函数中。
你创建了一个MessageService,以便在类之间实现松耦合通讯。
HeroService连同注入到它的服务MessageService一起,注入到了组件中。
https://www.cwiki.us/display/AngularZH/Services