iT邦幫忙

2021 iThome 鐵人賽

DAY 7
0
Modern Web

關於我作夢變成工程師這檔事(Angular 篇)系列 第 7

第 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


上一篇
第 6 天 調整 HeroDetail 的顯示方式|AppRoutingModule、ActivatedRoute
下一篇
第 8 天 邁出 RxJS 小小的一步|pipe、operators
系列文
關於我作夢變成工程師這檔事(Angular 篇)14

尚未有邦友留言

立即登入留言