英雄编辑器(03)-服务组件

创建一个 HeroService,应用中的所有类都可以使用它来获取英雄列表。 不要使用 new 关键字来创建此服务,而要依靠 Angular 的依赖注入机制把它注入到 HeroesComponent 的构造函数中。

服务是在多个“互相不知道”的类之间共享信息的好办法。 你将创建一个 MessageService,并且把它注入到两个地方:

1. 注入到 HeroService 中,它会使用该服务发送消息

2. 注入到 MessagesComponent 中,它会显示其中的消息。当用户点击某个英雄时,它还会显示该英雄的 ID。

创建 HeroService

使用 Angular CLI 创建一个名叫 hero 的服务。

ng generate service hero

@Injectable() 服务

注意,这个新的服务导入了 Angular 的 Injectable 符号,并且给这个服务类添加了 @Injectable() 装饰器。 它把这个类标记为依赖注入系统的参与者之一。HeroService 类将会提供一个可注入的服务,并且它还可以拥有自己的待注入的依赖。 目前它还没有依赖,但是很快就会有了。

@Injectable() 装饰器会接受该服务的元数据对象,就像 @Component() 对组件类的作用一样。

提供(provide) HeroService

你必须先注册一个服务提供者,来让 HeroService 在依赖注入系统中可用,Angular 才能把它注入到 HeroesComponent 中。所谓服务提供者就是某种可用来创建或交付一个服务的东西;在这里,它通过实例化 HeroService 类,来提供该服务。

为了确保 HeroService 可以提供该服务,就要使用注入器来注册它。注入器是一个对象,负责当应用要求获取它的实例时选择和注入该提供者。

默认情况下,Angular CLI 命令 ng generate service 会通过给 @Injectable() 装饰器添加 providedIn: 'root' 元数据的形式,用根注入器将你的服务注册成为提供者。

content_copy@Injectable({

  providedIn: 'root',

})

Angular 中的依赖注入

依赖注入(DI)是一种重要的应用设计模式。 Angular 有自己的 DI 框架,在设计应用时常会用到它,以提升它们的开发效率和模块化程度。

依赖,是当类需要执行其功能时,所需要的服务或对象。 DI 是一种编码模式,其中的类会从外部源中请求获取依赖,而不是自己创建它们。

在 Angular 中,DI 框架会在实例化该类时向其提供这个类所声明的依赖项。本指南介绍了 DI 在 Angular 中的工作原理,以及如何借助它来让你的应用更灵活、高效、健壮,以及可测试、可维护。

修改 HeroesComponent

打开 HeroesComponent 类文件。

删除 HEROES 的导入语句,因为你以后不会再用它了。 转而导入 HeroService。

src/app/heroes/heroes.component.ts (import HeroService)

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

把 heroes 属性的定义改为一句简单的声明。

src/app/heroes/heroes.component.ts

content_copyheroes: Hero[];

注入 HeroService

往构造函数中添加一个私有的 heroService,其类型为 HeroService。

src/app/heroes/heroes.component.ts

content_copyconstructor(private heroService: HeroService) {}

这个参数同时做了两件事:1. 声明了一个私有 heroService 属性,2. 把它标记为一个 HeroService 的注入点。

当 Angular 创建 HeroesComponent 时,依赖注入系统就会把这个 heroService 参数设置为 HeroService 的单例对象。

添加 getHeroes()

创建一个方法,以从服务中获取这些英雄数据。

src/app/heroes/heroes.component.ts

getHeroes(): void {

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

}

在 ngOnInit() 中调用它

你固然可以在构造函数中调用 getHeroes(),但那不是最佳实践。

让构造函数保持简单,只做初始化操作,比如把构造函数的参数赋值给属性。 构造函数不应该做任何事。 它当然不应该调用某个函数来向远端服务(比如真实的数据服务)发起 HTTP 请求。

而是选择在 ngOnInit 生命周期钩子中调用 getHeroes(),之后 Angular 会在构造出 HeroesComponent 的实例之后的某个合适的时机调用 ngOnInit()。

src/app/heroes/heroes.component.ts

ngOnInit() {

  this.getHeroes();

}

可观察对象版本的 HeroService

Observable 是 RxJS 库中的一个关键类。

在稍后的 HTTP 教程中,你就会知道 Angular HttpClient 的方法会返回 RxJS 的 Observable。 这节课,你将使用 RxJS 的 of() 函数来模拟从服务器返回数据。

打开 HeroService 文件,并从 RxJS 中导入 Observable 和 of 符号。

src/app/hero.service.ts (Observable imports)

import { Observable, of } from 'rxjs';

把 getHeroes() 方法改成这样:

src/app/hero.service.ts

getHeroes(): Observable<Hero[]> {

  return of(HEROES);

}

of(HEROES) 会返回一个 Observable<Hero[]>,它会发出单个值,这个值就是这些模拟英雄的数组。

在 HTTP 教程中,你将会调用 HttpClient.get<Hero[]>() 它也同样返回一个 Observable<Hero[]>,它也会发出单个值,这个值就是来自 HTTP 响应体中的英雄数组。

在 HeroesComponent 中订阅

HeroService.getHeroes 方法之前返回一个 Hero[], 现在它返回的是 Observable<Hero[]>。

你必须在 HeroesComponent 中也向本服务中的这种形式看齐。

找到 getHeroes 方法,并且把它替换为如下代码(和前一个版本对比显示):

heroes.component.ts (Observable)

content_copygetHeroes(): 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。

content_copyng generate component messages

CLI 在 src/app/messages 中创建了组件文件,并且把 MessagesComponent 声明在了 AppModule 中。

修改 AppComponent 的模板来显示所生成的 MessagesComponent:

src/app/app.component.html

content_copy<h1>{{title}}</h1>

<app-heroes></app-heroes>

<app-messages></app-messages>

你可以在页面的底部看到来自的 MessagesComponent 的默认内容。

创建 MessageService

使用 CLI 在 src/app 中创建 MessageService。

content_copyng generate service message

打开 MessageService,并把它的内容改成这样:

src/app/message.service.ts

content_copyimport { Injectable } from '@angular/core';

@Injectable({

  providedIn: 'root',

})

export class MessageService {

  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)

content_copyimport { MessageService } from './message.service';

修改这个构造函数,添加一个私有的 messageService 属性参数。 Angular 将会在创建 HeroService 时把 MessageService 的单例注入到这个属性中。

src/app/hero.service.ts

content_copyconstructor(private messageService: MessageService) { }

这是一个典型的“服务中的服务”场景: 你把 MessageService 注入到了 HeroService 中,而 HeroService 又被注入到了 HeroesComponent 中。

从 HeroService 中发送一条消息

修改 getHeroes() 方法,在获取到英雄数组时发送一条消息。

src/app/hero.service.ts

content_copygetHeroes(): Observable<Hero[]> {

  // TODO: send the message _after_ fetching the heroes

  this.messageService.add('HeroService: fetched heroes');

  return of(HEROES);

}

从 HeroService 中显示消息

MessagesComponent 可以显示所有消息, 包括当 HeroService 获取到英雄数据时发送的那条。

打开 MessagesComponent,并且导入 MessageService。

src/app/messages/messages.component.ts (import MessageService)

content_copyimport { MessageService } from '../message.service';

修改构造函数,添加一个 public 的 messageService 属性。 Angular 将会在创建 MessagesComponent 的实例时 把 MessageService 的实例注入到这个属性中。

src/app/messages/messages.component.ts

content_copyconstructor(public messageService: MessageService) {}

这个 messageService 属性必须是公共属性,因为你将会在模板中绑定到它。

Angular 只会绑定到组件的公共属性。

绑定到 MessageService

把 CLI 生成的 MessagesComponent 的模板改成这样:

src/app/messages/messages.component.html

content_copy<div *ngIf="messageService.messages.length">

  <h2>Messages</h2>

  <button class="clear"

          (click)="messageService.clear()">clear</button>

  <div *ngFor='let message of messageService.messages'> {{message}} </div>

</div>

这个模板直接绑定到了组件的 messageService 属性上。

• *ngIf 只有在有消息时才会显示消息区。

• *ngFor 用来在一系列 <div> 元素中展示消息列表。

• Angular 的事件绑定把按钮的 click 事件绑定到了 MessageService.clear()。

当你把 最终代码 某一页的内容添加到 messages.component.css 中时,这些消息会变得好看一些。

为 hero 服务添加额外的消息

下面的例子展示了当用户点击某个英雄时,如何发送和显示一条消息,以及如何显示该用户的选取历史。当你学到后面的路由一章时,这会很有帮助。

src/app/heroes/heroes.component.ts

content_copyimport { Component, OnInit } from '@angular/core';

import { Hero } from '../hero';

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

import { MessageService } from '../message.service';

@Component({

  selector: 'app-heroes',

  templateUrl: './heroes.component.html',

  styleUrls: ['./heroes.component.css']

})

export class HeroesComponent implements OnInit {

  selectedHero: Hero;

  heroes: Hero[];

  constructor(private heroService: HeroService, private messageService: MessageService) { }

  ngOnInit() {

    this.getHeroes();

  }

  onSelect(hero: Hero): void {

    this.selectedHero = hero;

    this.messageService.add(`HeroesComponent: Selected hero id=${hero.id}`);

  }

  getHeroes(): void {

    this.heroService.getHeroes()

        .subscribe(heroes => this.heroes = heroes);

  }

}

刷新浏览器,页面显示出了英雄列表。 滚动到底部,就会在消息区看到来自 HeroService 的消息。 点击“清空”按钮,消息区不见了。

查看最终代码

小结

• 你把数据访问逻辑重构到了 HeroService 类中。

• 你在根注入器中把 HeroService 注册为该服务的提供者,以便在别处可以注入它。

• 你使用 Angular 依赖注入机制把它注入到了组件中。

• 你给 HeroService 中获取数据的方法提供了一个异步的函数签名。

• 你发现了 Observable 以及 RxJS 库。

• 你使用 RxJS 的 of() 方法返回了一个模拟英雄数据的可观察对象 (Observable<Hero[]>)。

• 在组件的 ngOnInit 生命周期钩子中调用 HeroService 方法,而不是构造函数中。

• 你创建了一个 MessageService,以便在类之间实现松耦合通讯。

• HeroService 连同注入到它的服务 MessageService 一起,注入到了组件中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章