Hero guide 添加服務(wù)

2020-06-29 17:36 更新

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

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

服務(wù)存在的意義

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

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

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

  1. 注入到 HeroService 中,它會(huì)使用該服務(wù)發(fā)送消息

  1. 注入到 MessagesComponent 中,它會(huì)顯示其中的消息。當(dāng)用戶點(diǎn)擊某個(gè)英雄時(shí),它還會(huì)顯示該英雄的 ID。

創(chuàng)建 HeroService

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

  1. ng generate service hero

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

Path:"src/app/hero.service.ts (new service)"

  1. import { Injectable } from '@angular/core';
  2. @Injectable({
  3. providedIn: 'root',
  4. })
  5. export class HeroService {
  6. constructor() { }
  7. }

@Injectable() 服務(wù)

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

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

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

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

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

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

導(dǎo)入 Hero 和 HEROES。

Path:"src/app/hero.service.ts"

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

添加一個(gè) getHeroes 方法,讓它返回模擬的英雄列表。

Path:"src/app/hero.service.ts"

  1. getHeroes(): Hero[] {
  2. return HEROES;
  3. }

提供(provide) HeroService

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

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

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

  1. @Injectable({
  2. providedIn: 'root',
  3. })

注:
- 這是一個(gè)過(guò)渡性的代碼范例,它將會(huì)允許你提供并使用 HeroService。此刻的代碼和最終代碼相差很大。

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

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

修改 HeroesComponent

打開 HeroesComponent 類文件。

刪除 HEROES 的導(dǎo)入語(yǔ)句,因?yàn)槟阋院蟛粫?huì)再用它了。 轉(zhuǎn)而導(dǎo)入 HeroService。

Path:"src/app/heroes/heroes.component.ts (import HeroService)"

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

把 heroes 屬性的定義改為一句簡(jiǎn)單的聲明。

Path:"src/app/heroes/heroes.component.ts"

  1. heroes: Hero[];

注入 HeroService

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

Path:"src/app/heroes/heroes.component.ts"

  1. constructor(private heroService: HeroService) {}

這個(gè)參數(shù)同時(shí)做了兩件事:

  1. 聲明了一個(gè)私有 heroService 屬性。
  2. 把它標(biāo)記為一個(gè) HeroService 的注入點(diǎn)。

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

添加 getHeroes()

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

Path:"src/app/heroes/heroes.component.ts"

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

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

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

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

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

Path:"src/app/heroes/heroes.component.ts"

  1. getHeroes(): void {
  2. ngOnInit() {
  3. this.getHeroes();
  4. }

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

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

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

Path:"src/app/heroes/heroes.component.ts"

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

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

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

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

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

可觀察對(duì)象版本的 HeroService

Observable 是 RxJS 庫(kù)中的一個(gè)關(guān)鍵類。

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

打開 "HeroService" 文件,并從 RxJS 中導(dǎo)入 Observableof 符號(hào)。

Path:"src/app/hero.service.ts (Observable imports)"

  1. import { Observable, of } from 'rxjs';

getHeroes() 方法改成這樣:

Path:"src/app/hero.service.ts"

  1. getHeroes(): Observable<Hero[]> {
  2. return of(HEROES);
  3. }

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

在 HeroesComponent 中訂閱

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

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

找到 getHeroes 方法,并且把它替換為如下代碼(和前一個(gè)版本對(duì)比顯示):

  1. Path:"heroes.component.ts (Observable)"
    1. getHeroes(): void {
    2. this.heroService.getHeroes()
    3. .subscribe(heroes => this.heroes = heroes);
    4. }

  1. Path:"heroes.component.ts (Original)"
    1. getHeroes(): void {
    2. this.heroes = this.heroService.getHeroes();
    3. }

Observable.subscribe() 是關(guān)鍵的差異點(diǎn)。

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

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

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

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

顯示消息

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

  • 添加一個(gè) MessagesComponent,它在屏幕的底部顯示應(yīng)用中的消息。

  • 創(chuàng)建一個(gè)可注入的、全應(yīng)用級(jí)別的 MessageService,用于發(fā)送要顯示的消息。

  • MessageService 注入到 HeroService 中。

  • 當(dāng) HeroService 成功獲取了英雄數(shù)據(jù)時(shí)顯示一條消息。

創(chuàng)建 MessagesComponent

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

  1. ng generate component messages

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

修改 AppComponent 的模板來(lái)顯示所生成的 MessagesComponent

Path:"src/app/message.service.ts"

  1. import { Injectable } from '@angular/core';
  2. @Injectable({
  3. providedIn: 'root',
  4. })
  5. export class MessageService {
  6. messages: string[] = [];
  7. add(message: string) {
  8. this.messages.push(message);
  9. }
  10. clear() {
  11. this.messages = [];
  12. }
  13. }

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

注入到 HeroService 中

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

Path:"src/app/hero.service.ts (import MessageService)"

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

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

Path:"src/app/hero.service.ts"

  1. constructor(private messageService: MessageService) { }

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

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

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

Path:"src/app/hero.service.ts"

  1. getHeroes(): Observable<Hero[]> {
  2. // TODO: send the message _after_ fetching the heroes
  3. this.messageService.add('HeroService: fetched heroes');
  4. return of(HEROES);
  5. }

從 HeroService 中顯示消息

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

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

Path:"src/app/messages/messages.component.ts (import MessageService)"

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

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

Path:"src/app/messages/messages.component.ts"

  1. constructor(public messageService: MessageService) {}

這個(gè) messageService 屬性必須是公共屬性,因?yàn)槟銓?huì)在模板中綁定到它。

綁定到 MessageService

把 CLI 生成的 MessagesComponent 的模板改成這樣:

Path:"src/app/messages/messages.component.html"

  1. <div *ngIf="messageService.messages.length">
  2. <h2>Messages</h2>
  3. <button class="clear"
  4. (click)="messageService.clear()">clear</button>
  5. <div *ngFor='let message of messageService.messages'> {{message}} </div>
  6. </div>

這個(gè)模板直接綁定到了組件的 messageService 屬性上。

  • *ngIf 只有在有消息時(shí)才會(huì)顯示消息區(qū)。

  • *ngFor 用來(lái)在一系列 <div> 元素中展示消息列表。

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

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

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

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

Path:"src/app/heroes/heroes.component.ts"

  1. import { Component, OnInit } from '@angular/core';
  2. import { Hero } from '../hero';
  3. import { HeroService } from '../hero.service';
  4. import { MessageService } from '../message.service';
  5. @Component({
  6. selector: 'app-heroes',
  7. templateUrl: './heroes.component.html',
  8. styleUrls: ['./heroes.component.css']
  9. })
  10. export class HeroesComponent implements OnInit {
  11. selectedHero: Hero;
  12. heroes: Hero[];
  13. constructor(private heroService: HeroService, private messageService: MessageService) { }
  14. ngOnInit() {
  15. this.getHeroes();
  16. }
  17. onSelect(hero: Hero): void {
  18. this.selectedHero = hero;
  19. this.messageService.add(`HeroesComponent: Selected hero id=${hero.id}`);
  20. }
  21. getHeroes(): void {
  22. this.heroService.getHeroes()
  23. .subscribe(heroes => this.heroes = heroes);
  24. }
  25. }

刷新瀏覽器,頁(yè)面顯示出了英雄列表。 滾動(dòng)到底部,就會(huì)在消息區(qū)看到來(lái)自 HeroService 的消息。 點(diǎn)擊“清空”按鈕,消息區(qū)不見了。

查看最終代碼

  1. Path:"src/app/hero.service.ts"

  1. import { Injectable } from '@angular/core';
  2. import { Observable, of } from 'rxjs';
  3. import { Hero } from './hero';
  4. import { HEROES } from './mock-heroes';
  5. import { MessageService } from './message.service';
  6. @Injectable({
  7. providedIn: 'root',
  8. })
  9. export class HeroService {
  10. constructor(private messageService: MessageService) { }
  11. getHeroes(): Observable<Hero[]> {
  12. // TODO: send the message _after_ fetching the heroes
  13. this.messageService.add('HeroService: fetched heroes');
  14. return of(HEROES);
  15. }
  16. }

  1. Path:"src/app/message.service.ts"

  1. import { Injectable } from '@angular/core';
  2. @Injectable({
  3. providedIn: 'root',
  4. })
  5. export class MessageService {
  6. messages: string[] = [];
  7. add(message: string) {
  8. this.messages.push(message);
  9. }
  10. clear() {
  11. this.messages = [];
  12. }
  13. }

  1. Path:"src/app/heroes/heroes.component.ts"

  1. import { Component, OnInit } from '@angular/core';
  2. import { Hero } from '../hero';
  3. import { HeroService } from '../hero.service';
  4. import { MessageService } from '../message.service';
  5. @Component({
  6. selector: 'app-heroes',
  7. templateUrl: './heroes.component.html',
  8. styleUrls: ['./heroes.component.css']
  9. })
  10. export class HeroesComponent implements OnInit {
  11. selectedHero: Hero;
  12. heroes: Hero[];
  13. constructor(private heroService: HeroService, private messageService: MessageService) { }
  14. ngOnInit() {
  15. this.getHeroes();
  16. }
  17. onSelect(hero: Hero): void {
  18. this.selectedHero = hero;
  19. this.messageService.add(`HeroesComponent: Selected hero id=${hero.id}`);
  20. }
  21. getHeroes(): void {
  22. this.heroService.getHeroes()
  23. .subscribe(heroes => this.heroes = heroes);
  24. }
  25. }

  1. Path:"src/app/messages/messages.component.ts"

  1. import { Component, OnInit } from '@angular/core';
  2. import { MessageService } from '../message.service';
  3. @Component({
  4. selector: 'app-messages',
  5. templateUrl: './messages.component.html',
  6. styleUrls: ['./messages.component.css']
  7. })
  8. export class MessagesComponent implements OnInit {
  9. constructor(public messageService: MessageService) {}
  10. ngOnInit() {
  11. }
  12. }

  1. Path:"src/app/messages/messages.component.html"

  1. <div *ngIf="messageService.messages.length">
  2. <h2>Messages</h2>
  3. <button class="clear"
  4. (click)="messageService.clear()">clear</button>
  5. <div *ngFor='let message of messageService.messages'> {{message}} </div>
  6. </div>

  1. Path:"src/app/messages/messages.component.css"

  1. /* MessagesComponent's private CSS styles */
  2. h2 {
  3. color: red;
  4. font-family: Arial, Helvetica, sans-serif;
  5. font-weight: lighter;
  6. }
  7. body {
  8. margin: 2em;
  9. }
  10. body, input[text], button {
  11. color: crimson;
  12. font-family: Cambria, Georgia;
  13. }
  14. button.clear {
  15. font-family: Arial;
  16. background-color: #eee;
  17. border: none;
  18. padding: 5px 10px;
  19. border-radius: 4px;
  20. cursor: pointer;
  21. cursor: hand;
  22. }
  23. button:hover {
  24. background-color: #cfd8dc;
  25. }
  26. button:disabled {
  27. background-color: #eee;
  28. color: #aaa;
  29. cursor: auto;
  30. }
  31. button.clear {
  32. color: #333;
  33. margin-bottom: 12px;
  34. }

  1. Path:"src/app/app.module.ts"

  1. import { BrowserModule } from '@angular/platform-browser';
  2. import { NgModule } from '@angular/core';
  3. import { FormsModule } from '@angular/forms';
  4. import { AppComponent } from './app.component';
  5. import { HeroesComponent } from './heroes/heroes.component';
  6. import { HeroDetailComponent } from './hero-detail/hero-detail.component';
  7. import { MessagesComponent } from './messages/messages.component';
  8. @NgModule({
  9. declarations: [
  10. AppComponent,
  11. HeroesComponent,
  12. HeroDetailComponent,
  13. MessagesComponent
  14. ],
  15. imports: [
  16. BrowserModule,
  17. FormsModule
  18. ],
  19. providers: [
  20. // no need to place any providers due to the `providedIn` flag...
  21. ],
  22. bootstrap: [ AppComponent ]
  23. })
  24. export class AppModule { }

  1. Path:"src/app/app.component.html"

  1. <h1>{{title}}</h1>
  2. <app-heroes></app-heroes>
  3. <app-messages></app-messages>
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)