Angular 英雄之旅-添加服務(wù)

2022-07-19 10:46 更新

服務(wù)

英雄之旅的 ?HeroesComponent ?目前獲取和顯示的都是模擬數(shù)據(jù)。

本節(jié)課的重構(gòu)完成之后,?HeroesComponent? 變得更精簡,并且聚焦于為它的視圖提供支持。這也讓它更容易使用模擬服務(wù)進(jìn)行單元測試。

要查看本頁所講的范例程序,參閱現(xiàn)場演練 / 下載范例。

為什么需要服務(wù)

組件不應(yīng)該直接獲取或保存數(shù)據(jù),它們不應(yīng)該了解是否在展示假數(shù)據(jù)。它們應(yīng)該聚焦于展示數(shù)據(jù),而把數(shù)據(jù)訪問的職責(zé)委托給某個服務(wù)。

本節(jié)課,你將創(chuàng)建一個 ?HeroService?,應(yīng)用中的所有類都可以使用它來獲取英雄列表。 不要使用 new關(guān)鍵字來創(chuàng)建此服務(wù),而要依靠 Angular 的依賴注入機(jī)制把它注入到 ?HeroesComponent ?的構(gòu)造函數(shù)中。

服務(wù)是在多個“互相不知道”的類之間共享信息的好辦法。你將創(chuàng)建一個 ?MessageService?,并且把它注入到兩個地方。

  • 注入到 ?HeroService ?中,它會使用該服務(wù)發(fā)送消息
  • 注入到 ?MessagesComponent ?中,它會顯示其中的消息。當(dāng)用戶點(diǎn)擊某個英雄時,它還會顯示該英雄的 ID。

創(chuàng)建 HeroService

使用 Angular CLI 創(chuàng)建一個名叫 ?hero ?的服務(wù)。

ng generate service hero

該命令會在 ?src/app/hero.service.ts? 中生成 ?HeroService ?類的骨架,代碼如下:

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

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

  constructor() { }

}

@Injectable() 服務(wù)

注意,這個新的服務(wù)導(dǎo)入了 Angular 的 ?Injectable? 符號,并且給這個服務(wù)類添加了 ?@Injectable()? 裝飾器。 它把這個類標(biāo)記為依賴注入系統(tǒng)的參與者之一。?HeroService ?類將會提供一個可注入的服務(wù),并且它還可以擁有自己的待注入的依賴。 目前它還沒有依賴,但是很快就會有了。

?@Injectable()? 裝飾器會接受該服務(wù)的元數(shù)據(jù)對象,就像 ?@Component()? 對組件類的作用一樣。

獲取英雄數(shù)據(jù)

?HeroService ?可以從任何地方獲取數(shù)據(jù):Web 服務(wù)、本地存儲(LocalStorage)或一個模擬的數(shù)據(jù)源。

從組件中移除數(shù)據(jù)訪問邏輯,意味著將來任何時候你都可以改變目前的實(shí)現(xiàn)方式,而不用改動任何組件。這些組件不需要了解該服務(wù)的內(nèi)部實(shí)現(xiàn)。

這節(jié)課中的實(shí)現(xiàn)仍然會提供模擬的英雄列表

導(dǎo)入 ?Hero? 和 ?HEROES?。

import { Hero } from './hero';
import { HEROES } from './mock-heroes';

添加一個 ?getHeroes ?方法,讓它返回模擬的英雄列表

getHeroes(): Hero[] {
  return HEROES;
}

提供(provide)HeroService

你必須先注冊一個服務(wù)提供者,來讓 ?HeroService ?在依賴注入系統(tǒng)中可用,Angular 才能把它注入到 ?HeroesComponent ?中。所謂服務(wù)提供者就是某種可用來創(chuàng)建或交付一個服務(wù)的東西;在這里,它通過實(shí)例化 ?HeroService? 類,來提供該服務(wù)。

為了確保 ?HeroService ?可以提供該服務(wù),就要使用注入器來注冊它。注入器是一個對象,負(fù)責(zé)當(dāng)應(yīng)用要求獲取它的實(shí)例時選擇和注入該提供者。

默認(rèn)情況下,Angular CLI 命令 ?ng generate service? 會通過給 ?@Injectable()? 裝飾器添加 ?providedIn: 'root'? 元數(shù)據(jù)的形式,用根注入器將你的服務(wù)注冊成為提供者。

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

當(dāng)你在頂層提供該服務(wù)時,Angular 就會為 ?HeroService? 創(chuàng)建一個單一的、共享的實(shí)例,并把它注入到任何想要它的類上。在 ?@Injectable? 元數(shù)據(jù)中注冊該提供者,還能允許 Angular 通過移除那些完全沒有用過的服務(wù)來進(jìn)行優(yōu)化。

現(xiàn)在 ?HeroService ?已經(jīng)準(zhǔn)備好插入到 ?HeroesComponent ?中了。

這是一個過渡性的代碼范例,它將會允許你提供并使用 ?HeroService?。

修改 HeroesComponent

打開 ?HeroesComponent ?類文件。

刪除 ?HEROES ?的導(dǎo)入語句,因?yàn)槟阋院蟛粫儆盟?。轉(zhuǎn)而導(dǎo)入 ?HeroService?。

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

把 ?heroes ?屬性的定義改為一句簡單的聲明。

heroes: Hero[] = [];

注入 HeroService

往構(gòu)造函數(shù)中添加一個私有的 ?heroService?,其類型為 ?HeroService?。

constructor(private heroService: HeroService) {}

這個參數(shù)同時做了兩件事:1. 聲明了一個私有 ?heroService ?屬性,2. 把它標(biāo)記為一個 ?HeroService ?的注入點(diǎn)。

當(dāng) Angular 創(chuàng)建 ?HeroesComponent ?時,依賴注入系統(tǒng)就會把這個 ?heroService ?參數(shù)設(shè)置為 ?HeroService ?的單例對象。

添加 getHeroes()

創(chuàng)建一個方法,以從服務(wù)中獲取這些英雄數(shù)據(jù)。

getHeroes(): void {
  this.heroes = this.heroService.getHeroes();
}

在 ngOnInit() 中調(diào)用它

你固然可以在構(gòu)造函數(shù)中調(diào)用 ?getHeroes()?,但那不是最佳實(shí)踐。

讓構(gòu)造函數(shù)保持簡單,只做最小化的初始化操作,比如把構(gòu)造函數(shù)的參數(shù)賦值給屬性。構(gòu)造函數(shù)不應(yīng)該做任何事。它當(dāng)然不應(yīng)該調(diào)用某個函數(shù)來向遠(yuǎn)端服務(wù)(比如真實(shí)的數(shù)據(jù)服務(wù))發(fā)起 HTTP 請求。

而是選擇在 ngOnInit 生命周期鉤子中調(diào)用 ?getHeroes()?,之后 Angular 會在構(gòu)造出 ?HeroesComponent ?的實(shí)例之后的某個合適的時機(jī)調(diào)用 ?ngOnInit()?。

ngOnInit(): void {
  this.getHeroes();
}

查看運(yùn)行效果

刷新瀏覽器,該應(yīng)用仍運(yùn)行的一如既往。顯示英雄列表,并且當(dāng)你點(diǎn)擊某個英雄的名字時顯示出英雄詳情視圖。

可觀察(Observable)的數(shù)據(jù)

?HeroService.getHeroes()? 的函數(shù)簽名是同步的,它所隱含的假設(shè)是 ?HeroService ?總是能同步獲取英雄列表數(shù)據(jù)。而 ?HeroesComponent ?也同樣假設(shè)能同步取到 ?getHeroes()? 的結(jié)果。

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

這在真實(shí)的應(yīng)用中幾乎是不可能的。現(xiàn)在能這么做,只是因?yàn)槟壳霸摲?wù)返回的是模擬數(shù)據(jù)。不過很快,該應(yīng)用就要從遠(yuǎn)端服務(wù)器獲取英雄數(shù)據(jù)了,而那天生就是異步操作。

?HeroService ?必須等服務(wù)器給出響應(yīng),而 ?getHeroes()? 不能立即返回英雄數(shù)據(jù),瀏覽器也不會在該服務(wù)等待期間停止響應(yīng)。

?HeroService.getHeroes()? 必須具有某種形式的異步函數(shù)簽名

這節(jié)課,?HeroService.getHeroes()? 將會返回 ?Observable?,部分原因在于它最終會使用 Angular 的 ?HttpClient.get? 方法來獲取英雄數(shù)據(jù),而 ?HttpClient.get()? 會返回 ?Observable?。

可觀察對象版本的 HeroService

?Observable ?是 RxJS 庫中的一個關(guān)鍵類。

稍后的 HTTP 教程中,你就會知道 Angular ?HttpClient ?的方法會返回 RxJS 的 ?Observable?。這節(jié)課,你將使用 RxJS 的 ?of()? 函數(shù)來模擬從服務(wù)器返回數(shù)據(jù)。

打開 ?HeroService ?文件,并從 RxJS 中導(dǎo)入 ?Observable ?和 ?of ?符號。

import { Observable, of } from 'rxjs';

把 ?getHeroes()? 方法改成這樣:

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

?of(HEROES)? 會返回一個 ?Observable<Hero[]>?,它會發(fā)出單個值,這個值就是這些模擬英雄的數(shù)組。

在 HTTP 教程中,你將會調(diào)用 ?HttpClient.get<Hero[]>()? 它也同樣返回一個 ?Observable<Hero[]>?,它也會發(fā)出單個值,這個值就是來自 HTTP 響應(yīng)體中的英雄數(shù)組。

在 HeroesComponent 中訂閱

?HeroService.getHeroes? 方法之前返回一個 ?Hero[]?,現(xiàn)在它返回的是 ?Observable<Hero[]>?。

你必須在 ?HeroesComponent ?中也向本服務(wù)中的這種形式看齊。

找到 ?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()? 是關(guān)鍵的差異點(diǎn)。

上一個版本把英雄的數(shù)組賦值給了該組件的 ?heroes ?屬性。這種賦值是同步的,這里包含的假設(shè)是服務(wù)器能立即返回英雄數(shù)組或者瀏覽器能在等待服務(wù)器響應(yīng)時凍結(jié)界面。

當(dāng) ?HeroService ?真的向遠(yuǎn)端服務(wù)器發(fā)起請求時,這種方式就行不通了。

新的版本等待 ?Observable ?發(fā)出這個英雄數(shù)組,這可能立即發(fā)生,也可能會在幾分鐘之后。然后,?subscribe()? 方法把這個英雄數(shù)組傳給這個回調(diào)函數(shù),該函數(shù)把英雄數(shù)組賦值給組件的 ?heroes ?屬性。

使用這種異步方式,當(dāng) ?HeroService ?從遠(yuǎn)端服務(wù)器獲取英雄數(shù)據(jù)時,就可以工作了

顯示消息

這一節(jié)將指導(dǎo)你:

  • 添加一個 ?MessagesComponent?,它在屏幕的底部顯示應(yīng)用中的消息。
  • 創(chuàng)建一個可注入的、全應(yīng)用級別的 ?MessageService?,用于發(fā)送要顯示的消息。
  • 把 ?MessageService ?注入到 ?HeroService ?中。
  • 當(dāng) ?HeroService ?成功獲取了英雄數(shù)據(jù)時顯示一條消息。

創(chuàng)建 MessagesComponent

使用 CLI 創(chuàng)建 ?MessagesComponent?。

ng generate component messages

CLI 在 ?src/app/messages? 中創(chuàng)建了組件文件,并且把 ?MessagesComponent ?聲明在了 ?AppModule ?中。

修改 ?AppComponent ?的模板來顯示所生成的 ?MessagesComponent?。

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

你可以在頁面的底部看到來自的 ?MessagesComponent ?的默認(rèn)內(nèi)容。

創(chuàng)建 MessageService

使用 CLI 在 ?src/app? 中創(chuàng)建 ?MessageService?。

ng generate service message

打開 ?MessageService?,并把它的內(nèi)容改成這樣。

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

@Injectable({
  providedIn: 'root',
})
export class MessageService {
  messages: string[] = [];

  add(message: string) {
    this.messages.push(message);
  }

  clear() {
    this.messages = [];
  }
}

該服務(wù)對外暴露了它的 ?messages ?緩存,以及兩個方法:?add()? 方法往緩存中添加一條消息,?clear()? 方法用于清空緩存。

把它注入到 HeroService 中

在 ?HeroService ?中導(dǎo)入 ?MessageService?。

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

修改這個構(gòu)造函數(shù),添加一個私有的 ?messageService ?屬性參數(shù)。Angular 將會在創(chuàng)建 ?HeroService ?時把 ?MessageService ?的單例注入到這個屬性中。

constructor(private messageService: MessageService) { }

這是一個典型的“服務(wù)中的服務(wù)”場景:你把 ?MessageService ?注入到了 ?HeroService ?中,而 ?HeroService ?又被注入到了 ?HeroesComponent ?中。

從 HeroService 中發(fā)送一條消息

修改 ?getHeroes()? 方法,在獲取到英雄數(shù)組時發(fā)送一條消息。

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

從 HeroService 中顯示消息

?MessagesComponent ?可以顯示所有消息,包括當(dāng) ?HeroService ?獲取到英雄數(shù)據(jù)時發(fā)送的那條。

打開 ?MessagesComponent?,并且導(dǎo)入 ?MessageService?。

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

修改構(gòu)造函數(shù),添加一個 public 的 ?messageService ?屬性。Angular 將會在創(chuàng)建 ?MessagesComponent ?的實(shí)例時 把 ?MessageService ?的實(shí)例注入到這個屬性中。

constructor(public messageService: MessageService) {}

這個 ?messageService ?屬性必須是公共屬性,因?yàn)槟銓谀0逯薪壎ǖ剿?/p>

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

只有在有消息時才會顯示消息區(qū)。

*ngFor

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

Angular 事件綁定

把按鈕的 click 事件綁定到了 MessageService.clear()

當(dāng)你把 最終代碼 某一頁的內(nèi)容添加到 ?messages.component.css? 中時,這些消息會變得好看一些。

為 hero 服務(wù)添加額外的消息

下面的例子展示了當(dāng)用戶點(diǎn)擊某個英雄時,如何發(fā)送和顯示一條消息,以及如何顯示該用戶的選取歷史。當(dāng)你學(xué)到后面的路由一章時,這會很有幫助。

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);
  }
}

刷新瀏覽器,頁面顯示出了英雄列表。滾動到底部,就會在消息區(qū)看到來自 ?HeroService ?的消息。點(diǎn)擊 Clear messages 按鈕,消息區(qū)不見了。

查看最終代碼

下面是本頁所提到的源代碼。

  • 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>

小結(jié)

  • 你把數(shù)據(jù)訪問邏輯重構(gòu)到了 ?HeroService ?類中
  • 你在根注入器中把 ?HeroService ?注冊為該服務(wù)的提供者,以便在別處可以注入它
  • 你使用 Angular 依賴注入機(jī)制把它注入到了組件中
  • 你給 ?HeroService ?中獲取數(shù)據(jù)的方法提供了一個異步的函數(shù)簽名
  • 你發(fā)現(xiàn)了 ?Observable ?以及 RxJS 庫
  • 你使用 RxJS 的 ?of()? 方法返回了一個模擬英雄數(shù)據(jù)的可觀察對象 (?Observable<Hero[]>?)
  • 在組件的 ?ngOnInit ?生命周期鉤子中調(diào)用 ?HeroService ?方法,而不是構(gòu)造函數(shù)中
  • 你創(chuàng)建了一個 ?MessageService?,以便在類之間實(shí)現(xiàn)松耦合通訊
  • ?HeroService ?連同注入到它的服務(wù) ?MessageService ?一起,注入到了組件中


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號