NG2-我们创建一个可复用的服务来调用英雄的数据
《英雄指南》继续前行。接下来,我们准备添加更多的组件。
将来会有更多的组件访问英雄数据,我们不想一遍一遍地复制粘贴同样的代码。 解决方案是,创建一个单一的、可复用的数据服务,然后学着把它注入到那些需要它的组件中去。
我们将重构数据访问代码,把它隔离到一个独立的服务中去,让组件尽可能保持精简,专注于为视图提供支持。 在这种方式下,借助模拟服务来对组件进行单元测试也会更容易。
因为数据服务通常都是异步的,我们将在本章创建一个基于承诺 (Promise)的数据服务。
当然,一开始我们还是要让我们的程序运行起来。在终端输入
npm start
创建英雄服务
客户向我们描绘了本应用更大的目标:想要在不同的页面中用多种方式显示英雄。 现在我们已经能从列表中选择一个英雄了,但这还不够。 很快,我们将添加一个仪表盘来显示表现最好的英雄,并创建一个独立视图来编辑英雄的详情。 所有这些视图都需要英雄数据。
目前,AppComponent
显示的是模拟数据。 至少有两个地方可以改进: 首先,定义英雄的数据不该是组件的任务; 其次,想把这份英雄列表的数据共享给其它组件和视图可不那么容易。
我们可以把获取英雄数据的任务重构为一个单独的服务,它将提供英雄数据,并把服务在所有需要英雄数据的组件间共享。
创建 HeroService
在app
目录下创建一个名叫hero.service.ts
的文件。
我们的文件名遵循的规则是,小写的服务器名称加上.service后缀,如果服务名称包含多个单词,我们就把基本名部分写成中线形式 (dash-case)。 例如,SpecialSuperHeroService
服务应该被定义在special-super-hero.service.ts
文件中。
我们把这个类命名为HeroService
,并导出它,以供别人使用。
app/hero.service.ts
import { Injectable } from '@angular/core'; @Injectable()
export class HeroService {
}
可注入的服务
注意,我们导入了 Angular 的Injectable
函数,并作为@Injectable()
装饰器使用这个函数。
不要忘了写圆括号!如果忘了写,就会导致一个很难诊断的错误。
当 TypeScript 看到@Injectable()
装饰器时,就会记下本服务的元数据。 如果 Angular 需要往这个服务中注入其它依赖,就会使用这些元数据。
获取英雄数据
添加一个名叫getHeros
的方法。
@Injectable()
export class HeroService {
getHeroes(): void {} // stub
}
在这个实现上暂停一下,我们先来讲一个重点。
数据使用者并不知道本服务会如何获取数据。 我们的HeroService
服务可以从任何地方获取英雄的数据。 它可以从网络服务器获取,可以从浏览器的局部存储区获取,也可以从模拟的数据源。
这就是从组件中移除数据访问代码的美妙之处。 这样我们可以随时改变数据访问的实现方式,而无需对使用英雄的组件作任何改动。
模拟英雄数据
我们曾在AppComponent
组件中写过模拟数据。它不该在那里,但也不该在这里! 我们应把模拟数据移到它自己的文件中去。
从app.component.ts
文件中剪切HEROS
数组,把它粘贴到app
目录下一个名叫mock-heroes.ts
的文件中。 还要复制import {Hero}...
语句,因为我们的英雄数组用到了Hero
类
app/mock-heroes.ts
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'}
];
我们导出了HEROES
常量,以便可以在其它地方导入它 — 例如HeroService
服务。
同时,回到刚剪切出HEROES
数组的app.component.ts
文件,我们留下了一个尚未初始化的heroes
属性:
heroes: Hero[];
返回模拟的英雄数据
回到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;
}
}
使用 HeroService 服务
我们可以在多个组件中使用 HeroService 服务了,先从 AppComponent 开始。
通常,我们先导入要用的东西,例如HeroService
。
import { HeroService } from './hero.service';
导入这个服务让我们可以在代码中引用它。 AppComponent
该如何在运行中获得一个具体的HeroService
实例呢?
我们要自己 new 出这个 HeroService 吗?不!
尽管我们可以使用new
来创建HeroService
的实例,就像这样:
heroService = new HeroService(); // 不是这样的
但这不是个好主意,有很多理由,例如:
我们的组件得弄清楚该如何创建
HeroService
。 如果有一天我们修改了HeroService
的构造函数,我们不得不找出创建过此服务的每一处代码,并修改它。 围着补丁代码转圈很容易导致错误,还会增加测试负担。我们每次使用
new
都会创建一个新的服务实例。 如果这个服务需要缓存英雄列表,并把这个缓存共享给别人呢?怎么办? 没办法,做不到。我们把
AppComponent
锁定到HeroService
的一个特定实现。 我们很难在不同的场景中切换实现。 例如,能离线操作吗?能在测试时使用不同的模拟版本吗?这可不容易。如果……如果……嘿!这下我们可有得忙了!
有办法了,真的!这个办法真是简单得不可思议,它能解决这些问题,你就再也没有犯错误的借口了。
注入 HeroService
用这两行代码代替用new
时的一行:
添加一个构造函数,并定义一个私有属性。
添加组件的
providers
元数据。
下面就是这个构造函数:
constructor(private heroService: HeroService) { }
构造函数自己什么也不用做,它在参数中定义了一个私有的heroService
属性,并把它标记为注入HeroService
的靶点。
现在,当创建AppComponent
实例时,Angular 知道需要先提供一个HeroService
的实例。
看到这里,是不是感到很兴奋很熟悉,构造函数注入。
注入器还不知道该如何创建HeroService
。 如果现在运行我们的代码,Angular 就会失败,并报错:
EXCEPTION: No provider for HeroService! (AppComponent -> HeroService)
(异常:没有 HeroService 的提供商!(AppComponent -> HeroService))
有么有?
我们还得注册一个HeroService
提供商,来告诉注入器如何创建HeroService
。 要做到这一点,我们在@Component
组件的元数据底部添加providers
数组属性如下:
providers:[HeroService]
providers
数组告诉 Angular,当它创建新的AppComponent
组件时,也要创建一个HeroService
的新实例。 AppComponent
会使用那个服务来获取英雄列表,在它组件树中的每一个子组件也同样如此。
AppComponent 中的 getHeroes
我们已经有了服务,并把它存入了私有变量heroService
中。我们这就开始使用它。
停下来想一想。我们可以在同一行内调用此服务并获得数据。
this.heroes = this.heroService.getHeroes();
在真实的世界中,我们并不需要把一行代码包装成一个专门的方法,但无论如何,我们在演示代码中先这么写:
getHeros():void{
this.heroes = this.heroService.getHeroes();
}
ngOnInit 生命周期钩子
毫无疑问,AppComponent
应该获取英雄数据并显示它。 我们该在哪里调用getHeroes
方法呢?在构造函数中吗? 不 !
多年的经验和惨痛的教训教育我们,应该把复杂的逻辑扔到构造函数外面去, 特别是那些需要从服务器获取数据的逻辑更是如此。
构造函数是为了简单的初始化工作而设计的,例如把构造函数的参数赋值给属性。 它的负担不应该过于沉重。我们应该能在测试中创建一个组件,而不用担心它会做实际的工作 — 例如和服务器通讯,直到我们主动要求它做这些。
如果不在构造函数中,总得有地方调用getHeroes
吧。
这也不难。只要我们实现了 Angular 的 ngOnInit 生命周期钩子,Angular 就会主动调用这个钩子。 Angular提供了一些接口,用来介入组件生命周期的几个关键时间点:刚创建时、每次变化时,以及最终被销毁时。
每个接口都有唯一的一个方法。只要组件实现了这个方法,Angular 就会在合适的时机调用它。
这是OnInit
接口的基本轮廓:
import { OnInit } from '@angular/core'; export class AppComponent implements OnInit {
ngOnInit(): void {
}
}
我们写了一个带有初始化逻辑的ngOnInit
方法,Angular会在适当的时候调用它。 在这个例子中,我们通过调用getHeroes
来完成初始化。
ngOnit():void{
this.getHeroes();
}
我们的应用将会像期望的那样运行,显示英雄列表,并且在我们点击英雄的名字时,显示英雄的详情。
我们就快完成了,但还有点事情不太对劲。
异步服务与承诺
我们的HeroService
立即返回一个模拟的英雄列表,它的getHeroes
函数签名是同步的。
this.heroes = this.heroService.getHeroes();
请求英雄数据,返回结果中就有它们了。
将来,我们打算从远端服务器上获取英雄数据。我们还没调用 http,但在后面的章节中我们会希望这么做。
那时候,我们不得不等待服务器响应,并且在等待过程中我们无法阻塞用户界面响应, 即使我们想这么做(也不应这么做)也做不到,因为浏览器不会阻塞。(这里为什么要提到堵塞,因为后面有一个及时搜索显示结果的例子)
我们不得不使用一些异步技术,而这将改变getHeroes
方法的签名。
我们将使用 承诺 。
HeroService
会生成一个承诺
承诺 就是 …… 好吧,它就是一个承诺,在有了结果时,它承诺会回调我们。 我们请求一个异步服务去做点什么,并且给它一个回调函数。 它会去做(在某个地方),一旦完成,它就会调用我们的回调函数,并通过参数把工作结果或者错误信息传给我们。
把HeroService
的getHeroes
方法改写为返回承诺的形式:
getHeroes():Promise<Hero[]>{ return Promise.resolve(HEROES);
}
我们继续使用模拟数据。我们通过返回一个 立即解决的承诺 的方式,模拟了一个超快、零延迟的超级服务器。
基于承诺的行动
回到AppComponent
和它的getHeroes
方法,我们看到它看起来还是这样的:
app/app.component.ts (getHeroes - old)
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}
在修改了HeroService
之后,我们还要把this.heroes
替换为一个承诺,而不再是一个英雄数组。
我们得修改这个实现,把它变成基于承诺的,并在承诺的事情被解决时再行动。 一旦承诺的事情被成功解决,我们就会显示英雄数据。
我们把回调函数作为参数传给承诺对象的then方法:
app/app.component.ts (getHeroes - revised)
getHeroes(): void {
this.heroService.getHeroes().then(heroes => this.heroes = heroes);
}
在回调函数中,我们把服务返回的英雄数组赋值给组件的heroes
属性。是的,这就搞定了。
我们的程序仍在运行,仍在显示英雄列表,在选择英雄时,仍然会把它/她显示在详情页面中。
下面是本章讨论过的代码文件:
app/hero.service.ts
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);
}
}
app/app.component.ts
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;
}
}
app/mock-heroes.ts
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'}
];
走过的路
来盘点一下我们完成了什么。
我们创建了一个能被多个组件共享的服务类。
我们使用了
ngOnInit
生命周期钩子,以便在AppComponent
激活时获取英雄数据。我们把
HeroService
定义为AppComponent
的一个提供商。我们创建了模拟的英雄数据,并把它导入我们的服务中。
我们把服务设计为返回承诺,组件从承诺中获取数据。
前方的路
通过使用共享组件和服务,我们的《英雄指南》更有复用性了。 我们还要创建一个仪表盘,要添加在视图间路由的菜单链接,还要在模板中格式化数据。 随着我们应用的进化,我们还会学到如何进行设计,让它更易于扩展和维护。
我们将在下一章学习 Angular 组件路由,以及在视图间导航的知识。
附件:慢一点
我们可以模拟慢速连接。
导入Hero
类,并且在HeroService
中添加如下的getHeroesSlowly
方法:
getHeroesSlowly(): Promise<Hero[]> {
return new Promise<Hero[]>(resolve =>
setTimeout(resolve, 2000)) // delay 2 seconds
.then(() => this.getHeroes());
}
像getHeroes
一样,它也返回一个承诺。 但是,这个承诺会在提供模拟数据之前等待两秒钟。
回到AppComponent
,用heroService.getHeroesSlowly
替换heroService.getHeroes
,并观察应用的行为。
下一步,我们将学习路由
NG2-我们创建一个可复用的服务来调用英雄的数据的更多相关文章
- 使用MicroService4Net 快速创建一个简单的微服务
“微服务架构(Microservice Architecture)”一词在过去几年里广泛的传播,它用于描述一种设计应用程序的特别方式,作为一套独立可部署的服务.目前,这种架构方式还没有准确的定义,但是 ...
- 微服务配置内容《网上copy》=========》如何创建一个高可用的服务注册中心
前言:首先要知道什么是一个高可用的服务注册中心,基于spring boot建成的服务注册中心是一个单节点的服务注册中心,这样一旦发生了故障,那么整个服务就会瘫痪,所以我们需要一个高可用的服务注册中心, ...
- 如何创建一个标准的Windows服务
出处:http://www.cnblogs.com/wuhuacong/archive/2009/02/11/1381428.html 如何创建一个标准的Windows服务 在很多时候,我们需要一个定 ...
- node创建一个简单的web服务
本文将如何用node创建一个简单的web服务,过程也很简单呢~ 开始之前要先安装node.js 1.创建一个最简单的服务 // server.js const http = require('http ...
- 使用Axis2创建一个简单的WebService服务
使用过Java进行过WebService开发都会听过或者接触过Apache Axis2,Axis2框架是应用最广泛的WebService框架之一了. 这里使用Axis2来开发和部署一个最简单的WebS ...
- 创建一个简单的windows服务,每间隔一定时间重复执行批处理文件
创建一个windows服务项目,增加App.config <?xml version="1.0" encoding="utf-8" ?> <c ...
- 使用python2与python3创建一个简单的http服务(基于SimpleHTTPServer)
python2与python3基于SimpleHTTPServer创建一个http服务的方法是不同的: 一.在linux服务器上面检查一下自己的python版本:如: [root@zabbix ~]# ...
- nodejs创建一个简单的web服务
这是一个突如其来的想法,毕竟做web服务的框架那么多,为什么要选择nodejs,因为玩前端时,偶尔想调用接口获取数据,而不想关注业务逻辑,只是想获取数据,使用java或者.net每次修改更新后还要打包 ...
- 020、MySQL创建一个存储过程,显示存储过程,调用存储过程,删除存储过程
一.我们创建一个MySQL储存过程,在SQL代码区写入以下内容,并执行就可以了 #编写一个存储过程 CREATE PROCEDURE ShowDate ( ) BEGIN #输出当前时间 SELECT ...
随机推荐
- MongoDB内嵌文档操作
实体定义: [BsonIgnoreExtraElements] public class Person : BaseEntity { public string FirstName { get; se ...
- Python-第三方库requests
Requests 是使用 Apache2 Licensed 许可证的 基于Python开发的HTTP 库,其在Python内置模块的基础上进行了高度的封装,从而使得Pythoner进行网络请求时,变得 ...
- SQL Server实时同步更新远程数据库遇到的问题
工作中遇到这样的情况,需要在更新表TableA(位于服务器ServerA 172.16.8.100中的库DatabaseA)同时更新TableB(位于服务器ServerB 172.16.8.101中的 ...
- 在C#里面获得应用程序的当前路径
Environment.CurrentDirectory——获取应用程序的当前工作目录.System.IO.Directory.GetCurrentDirectory()AppDomain.Curre ...
- IBM X3650 M4 主板故障
故障描述: 今天突然接到报警,一台服务器无法连通,无法登录.无法 ping 通. 打电话到 IDC ,授权工程师查看服务器状态,返回结果如下: 1.服务器关机状态 2.无法开机 ( 电源灯亮 ),按开 ...
- MyBatis 注解式开发
- layer插件open方法回掉值问题
最近做项目需用到一个弹出层加载iframe页面的东西,首先想到layer插件,此插件用到过多次,兼容性很好,功能也强大,废话不多说上代码. 其实功能很简单,就是在目标页面选择一个值,回掉回来,说明一下 ...
- Multithread之为什么spinlock必须是volatile?
[Multithread之为什么spinlock必须是volatile?] 1.编译器的优化 在本次线程内,当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中:以后再取变量 ...
- Android中asset文件夹与raw文件夹的区别深入解析(转)
*res/raw和assets的相同点:1.两者目录下的文件在打包后会原封不动的保存在apk包中,不会被编译成二进制.*res/raw和assets的不同点:1.res/raw中的文件会被映射到R.j ...
- Oracle_in_not-in_distinct_minsu的用法
create table a( id int, username ) ); create table b( id int, username ) ); ,'小明'); ,'小红'); ,'小君'); ...