第 7 天 让元件归元件、服务归服务|service、@Injectable、AsyncPipe

前情提要

「英雄之旅」已经可以浏览完整的英雄列表,并透过路由参数来取得特定的英雄资料,达到换页浏览细节资讯的功能,可以说,我们初步完成了英雄的资料查询。在我们进一步完成对资料的增、删、改之前,先要来重构程序码:将元件中与资料互动的相关逻辑移走——让元件专注在展示资料上,而关於与资料互动的逻辑,我们将新增服务(service)来统一管理。

新增服务

在一般的情境下,一个服务应该是整个 app 共用的。如此一来,不管这个服务注入在哪个元件中(被哪个元件、功能使用),都能够确保它的状态(资料)是一致的。因此,我们在 shared 资料夹下新增 services 资料夹来管理 service 档案,并在此资料夹执行指令:

ng g s hero // g for generate; s for service

档案目录如下:

src
⌞app
  ⌞ shared
      ⌞ models
      ⌞ services
          hero.service.ts

打开 hero.service.ts 档案:

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

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

  constructor() { }

}

可以注意到服务其实也是个类别(class),此外,@Injectable 装饰器是非常重要的,因为:

  1. 服务通常需要在 constructor 依赖注入其他的类别,例如 HttpClient。因此,透过 @Injectable 装饰器,可以如同 @Component 装饰器一样,在 constructor 自动完成依赖注入。举例来说,可以用下列方式很快地依赖注入 HttpClient,只需要在 construcotr 输入一行 private http: HttpClient(当然还需要在上面 import HttpClient):
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

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

  constructor(
    private http: HttpClient
  ) { }
  
}
  1. 在元资料物件中标明这是应用程序级的服务,其他特殊使用情况请参考文件
@Injectable({
  providedIn: 'root' // 应用程序级服务,确保注入此服务的地方,状态(资料)是一致的。
})

重构与资料互动的程序

目前在 App 中,拥有两个与资料互动的地方:

  • 在 HeroListComponent 中,取得所有英雄资料。
  • 在 HeroDetailComponent 中,传送参数取得个别英雄资料。

先在 hero.service.ts 来撰写取得所有英雄资料的方法:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs';

import { Hero } from './../models/hero.model';
@Injectable({
  providedIn: 'root'
})
export class HeroService {

  constructor(
    private http: HttpClient
  ) { }

  getHeroes(): Observable<Hero[]> {
    return this.http.get<Hero[]>(`api/heroes`);
  }

我们主要关注 getHeroes():

  getHeroes(): Observable<Hero[]> {
    return this.http.get<Hero[]>(`api/heroes`);
  }

可以看到,这个方法会回传的资料型态是 Observable<Hero[]>,Observable 是 RxJS 的术语,意思是可观察的。在 Angular,使用 HttpClient 的 get 方法回传的资料预设都是 Observable。

接着,在 hero-list.component.ts 依赖注入 HeroService,并调整取得所有英雄资料的方法:

import { HeroService } from './../shared/services/hero.service';
import { Component, OnInit } from '@angular/core';

import { Hero } from './../shared/models/hero.model';
@Component({
  selector: 'app-hero-list',
  templateUrl: './hero-list.component.html',
  styleUrls: ['./hero-list.component.css']
})
export class HeroListComponent implements OnInit {

  heroList: Hero[] = [];

  constructor(
    private heroService: HeroService
  ) {}

  ngOnInit(): void {
    this.heroService.getHeroes().subscribe((heroList) => {
      this.heroList = heroList;
    })
  }

}

程序码几乎相同,但取得资料的种种逻辑被移到 HeroService 了——我们不必在 HeroListComponent 知道这些资讯。

除了上面的使用方法之外,因为 HttpClient 回传的资料是可观察的(Observable),因此我们可以使用下列的方法来实作。首先调整 hero-list.component.ts

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

import { Observable } from 'rxjs';

import { HeroService } from './../shared/services/hero.service';
import { Hero } from './../shared/models/hero.model';

@Component({
  selector: 'app-hero-list',
  templateUrl: './hero-list.component.html',
  styleUrls: ['./hero-list.component.css']
})
export class HeroListComponent implements OnInit {

  heroList$: Observable<Hero[]>;

  constructor(
    private heroService: HeroService
  ) {
    this.heroList$ = this.heroService.getHeroes();
  }

  ngOnInit(): void {}

}

我们将原本的属性 heroList: Hero[] 调整为 heroList$: Observable<Hero[]>$ 是 RxJS 的惯用写法,代表这是一个可以被观察(订阅)的属性——这里也可以注意到,原本在 hero-list.component.ts 中的订阅(subscribe)资料的行为消失了。因为,我们将使用 Angular 提供的 Async 管道来订阅它,让我们在画面档案 hero-list.component.html 来完成这件事:

<div class="hero-container" *ngIf="heroList$ | async as heroList">
  <mat-card class="hero-item" *ngFor="let hero of heroList">
      (略)
  </mat-card>
</div>

在 div 上我们使用了 *ngIf 指令,放置 heroList$ 属性并使用 async 管道,这个管道将订阅 heroList$,也就是 HeroService 服务的 getHeroes() 回传的资料:

  getHeroes(): Observable<Hero[]> {
    return this.http.get<Hero[]>(`api/heroes`);
  }

当有接收到资料时,*ngIf 就会为 true,以下的所有标签就会建立出来(显示在画面上)。同时,得到的资料被指派为变数 heroList (as heroList)。这与原先的程序码是相同的(*ngFor="let hero of heroList"),因此其他程序码并不需要改动。

藉由将 getHeroes() 移到 HeroService 并改为 RxJS 的写法,我们的程序码更为精简。而另外一个方法 getHero(heroId) 将更深入地使用到 RxJS 的 operator,这是我们明天要来完成的事:「极简 RxJS 使用方法」。打完这几个字我自己都抖了起来,赶快睡觉压压惊 :P。

程序码已推上 Github


<<:  [ Day 07 ] Class Component

>>:  Day09:程序码编辑器的实用快捷键(1)

[Day 01] 前言、文章大纲

前言 Sass对现在前端来说已经是不可或缺的技术,不仅用起来顺手,如果熟悉之後切版的速度将会加快非常...

Day22 - ArgoCD 建立应用程序

前言 前一天我们在 K8s Cluster 建置好了 ArgoCD 服务,今天就来实际操作看看,透过...

[Day 29] Optimize Images

取自 Artifact Austin: Leaving Pixels Behind - Todd ...

使用 package 来管理类别吧!

昨天我们虽然各别把 .java 跟 .class 分类到不同的资料夹, 但长久下来还是不够的,我们还...

无线网路篇(Wi-Fi)

今天内容跟我原本预期的,出入满多QQ 因为先前有买一的书,叫「黑客大揭秘 近源渗透测试」, 本来打算...