阅读(3961) (0)

Angular 英雄之旅-添加服务

2022-07-12 17:38:16 更新

服务

英雄之旅的 ​HeroesComponent ​目前获取和显示的都是模拟数据。

本节课的重构完成之后,​HeroesComponent​ 变得更精简,并且聚焦于为它的视图提供支持。这也让它更容易使用模拟服务进行单元测试。

要查看本页所讲的范例程序,参阅现场演练 / 下载范例

为什么需要服务

组件不应该直接获取或保存数据,它们不应该了解是否在展示假数据。它们应该聚焦于展示数据,而把数据访问的职责委托给某个服务。

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

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

  • 注入到 ​HeroService ​中,它会使用该服务发送消息
  • 注入到 ​MessagesComponent ​中,它会显示其中的消息。当用户点击某个英雄时,它还会显示该英雄的 ID。

创建 HeroService

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

ng generate service hero

该命令会在 ​src/app/hero.service.ts​ 中生成 ​HeroService ​类的骨架,代码如下:

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

@Injectable({
  providedIn: 'root',
})
export class HeroService {

  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[] {
  return HEROES;
}

提供(provide)HeroService

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

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

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

@Injectable({
  providedIn: 'root',
})

当你在顶层提供该服务时,Angular 就会为 ​HeroService​ 创建一个单一的、共享的实例,并把它注入到任何想要它的类上。在 ​@Injectable​ 元数据中注册该提供者,还能允许 Angular 通过移除那些完全没有用过的服务来进行优化。

现在 ​HeroService ​已经准备好插入到 ​HeroesComponent ​中了。

这是一个过渡性的代码范例,它将会允许你提供并使用 ​HeroService​。

修改 HeroesComponent

打开 ​HeroesComponent ​类文件。

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

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

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

heroes: Hero[] = [];

注入 HeroService

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

constructor(private heroService: 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(): void {
  this.getHeroes();
}

查看运行效果

刷新浏览器,该应用仍运行的一如既往。显示英雄列表,并且当你点击某个英雄的名字时显示出英雄详情视图。

可观察(Observable)的数据

HeroService.getHeroes()​ 的函数签名是同步的,它所隐含的假设是 ​HeroService ​总是能同步获取英雄列表数据。而 ​HeroesComponent ​也同样假设能同步取到 ​getHeroes()​ 的结果。

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

这在真实的应用中几乎是不可能的。现在能这么做,只是因为目前该服务返回的是模拟数据。不过很快,该应用就要从远端服务器获取英雄数据了,而那天生就是异步操作。

HeroService ​必须等服务器给出响应,而 ​getHeroes()​ 不能立即返回英雄数据,浏览器也不会在该服务等待期间停止响应。

HeroService.getHeroes()​ 必须具有某种形式的异步函数签名

这节课,​HeroService.getHeroes()​ 将会返回 ​Observable​,部分原因在于它最终会使用 Angular 的 ​HttpClient.get​ 方法来获取英雄数据,而 ​HttpClient.get()​ 会返回 ​Observable​。

可观察对象版本的 HeroService

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

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

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

import { Observable, of } from 'rxjs';

把 ​getHeroes()​ 方法改成这样:

getHeroes(): Observable<Hero[]> {
  const heroes = of(HEROES);
  return heroes;
}

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

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

在 HeroesComponent 中订阅

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

你必须在 ​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​。

<h1>{{title}}</h1>
<app-heroes></app-heroes>
<app-messages></app-messages>

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

创建 MessageService

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

ng generate service message

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

import { 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​。

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

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

constructor(private messageService: MessageService) { }

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

从 HeroService 中发送一条消息

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

getHeroes(): Observable<Hero[]> {
  const heroes = of(HEROES);
  this.messageService.add('HeroService: fetched heroes');
  return heroes;
}

从 HeroService 中显示消息

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

打开 ​MessagesComponent​,并且导入 ​MessageService​。

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

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

constructor(public messageService: MessageService) {}

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

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

绑定到 MessageService

把 CLI 生成的 ​MessagesComponent ​的模板改成这样。

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

  <h2>Messages</h2>
  <button type="button" class="clear"
          (click)="messageService.clear()">Clear messages</button>
  <div *ngFor='let message of messageService.messages'> {{message}} </div>

</div>

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

详情

*ngIf

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

*ngFor

在一系列 <div> 元素中展示消息列表。

Angular 事件绑定

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

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

为 hero 服务添加额外的消息

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

import { 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(): void {
    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 ​的消息。点击 Clear messages 按钮,消息区不见了。

查看最终代码

下面是本页所提到的源代码。

  • src/app/hero.service.ts
  • import { Injectable } from '@angular/core';
    
    import { Observable, of } from 'rxjs';
    
    import { Hero } from './hero';
    import { HEROES } from './mock-heroes';
    import { MessageService } from './message.service';
    
    @Injectable({
      providedIn: 'root',
    })
    export class HeroService {
    
      constructor(private messageService: MessageService) { }
    
      getHeroes(): Observable<Hero[]> {
        const heroes = of(HEROES);
        this.messageService.add('HeroService: fetched heroes');
        return heroes;
      }
    }
  • src/app/message.service.ts
  • import { Injectable } from '@angular/core';
    
    @Injectable({
      providedIn: 'root',
    })
    export class MessageService {
      messages: string[] = [];
    
      add(message: string) {
        this.messages.push(message);
      }
    
      clear() {
        this.messages = [];
      }
    }
  • src/app/heroes/heroes.component.ts
  • import { 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(): void {
        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);
      }
    }
  • src/app/messages/messages.component.ts
  • import { Component, OnInit } from '@angular/core';
    import { MessageService } from '../message.service';
    
    @Component({
      selector: 'app-messages',
      templateUrl: './messages.component.html',
      styleUrls: ['./messages.component.css']
    })
    export class MessagesComponent implements OnInit {
    
      constructor(public messageService: MessageService) {}
    
      ngOnInit() {
      }
    
    }
  • src/app/messages/messages.component.html
  • <div *ngIf="messageService.messages.length">
    
      <h2>Messages</h2>
      <button type="button" class="clear"
              (click)="messageService.clear()">Clear messages</button>
      <div *ngFor='let message of messageService.messages'> {{message}} </div>
    
    </div>
  • src/app/messages/messages.component.css
  • /* MessagesComponent's private CSS styles */
    h2 {
      color: #A80000;
      font-family: Arial, Helvetica, sans-serif;
      font-weight: lighter;
    }
    
    .clear {
      color: #333;
      background-color: #eee;
      margin-bottom: 12px;
      padding: 1rem;
      border-radius: 4px;
      font-size: 1rem;
    }
    .clear:hover {
      color: white;
      background-color: #42545C;
    }
  • src/app/app.module.ts
  • import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { AppComponent } from './app.component';
    import { HeroesComponent } from './heroes/heroes.component';
    import { HeroDetailComponent } from './hero-detail/hero-detail.component';
    import { MessagesComponent } from './messages/messages.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        HeroesComponent,
        HeroDetailComponent,
        MessagesComponent
      ],
      imports: [
        BrowserModule,
        FormsModule
      ],
      providers: [
        // no need to place any providers due to the `providedIn` flag...
      ],
      bootstrap: [ AppComponent ]
    })
    export class AppModule { }
  • src/app/app.component.html
  • <h1>{{title}}</h1>
    <app-heroes></app-heroes>
    <app-messages></app-messages>

小结

  • 你把数据访问逻辑重构到了 ​HeroService ​类中
  • 你在根注入器中把 ​HeroService ​注册为该服务的提供者,以便在别处可以注入它
  • 你使用 Angular 依赖注入机制把它注入到了组件中
  • 你给 ​HeroService ​中获取数据的方法提供了一个异步的函数签名
  • 你发现了 ​Observable ​以及 RxJS 库
  • 你使用 RxJS 的 ​of()​ 方法返回了一个模拟英雄数据的可观察对象 (​Observable<Hero[]>​)
  • 在组件的 ​ngOnInit ​生命周期钩子中调用 ​HeroService ​方法,而不是构造函数中
  • 你创建了一个 ​MessageService​,以便在类之间实现松耦合通讯
  • HeroService ​连同注入到它的服务 ​MessageService ​一起,注入到了组件中