創建一個 HeroService,應用中的所有類都可以使用它來獲取英雄列表。 不要使用 new 關鍵字來創建此服務,而要依靠 Angular 的依賴注入機制把它注入到 HeroesComponent 的構造函數中。
服務是在多個“互相不知道”的類之間共享信息的好辦法。 你將創建一個 MessageService,並且把它注入到兩個地方:
1. 注入到 HeroService 中,它會使用該服務發送消息
2. 注入到 MessagesComponent 中,它會顯示其中的消息。當用戶點擊某個英雄時,它還會顯示該英雄的 ID。
創建 HeroService
使用 Angular CLI 創建一個名叫 hero 的服務。
ng generate service hero
@Injectable() 服務
注意,這個新的服務導入了 Angular 的 Injectable 符號,並且給這個服務類添加了 @Injectable() 裝飾器。 它把這個類標記爲依賴注入系統的參與者之一。HeroService 類將會提供一個可注入的服務,並且它還可以擁有自己的待注入的依賴。 目前它還沒有依賴,但是很快就會有了。
@Injectable() 裝飾器會接受該服務的元數據對象,就像 @Component() 對組件類的作用一樣。
提供(provide) HeroService
你必須先註冊一個服務提供者,來讓 HeroService 在依賴注入系統中可用,Angular 才能把它注入到 HeroesComponent 中。所謂服務提供者就是某種可用來創建或交付一個服務的東西;在這裏,它通過實例化 HeroService 類,來提供該服務。
爲了確保 HeroService 可以提供該服務,就要使用注入器來註冊它。注入器是一個對象,負責當應用要求獲取它的實例時選擇和注入該提供者。
默認情況下,Angular CLI 命令 ng generate service 會通過給 @Injectable() 裝飾器添加 providedIn: 'root' 元數據的形式,用根注入器將你的服務註冊成爲提供者。
content_copy@Injectable({
providedIn: 'root',
})
Angular 中的依賴注入
依賴注入(DI)是一種重要的應用設計模式。 Angular 有自己的 DI 框架,在設計應用時常會用到它,以提升它們的開發效率和模塊化程度。
依賴,是當類需要執行其功能時,所需要的服務或對象。 DI 是一種編碼模式,其中的類會從外部源中請求獲取依賴,而不是自己創建它們。
在 Angular 中,DI 框架會在實例化該類時向其提供這個類所聲明的依賴項。本指南介紹了 DI 在 Angular 中的工作原理,以及如何藉助它來讓你的應用更靈活、高效、健壯,以及可測試、可維護。
修改 HeroesComponent
打開 HeroesComponent 類文件。
刪除 HEROES 的導入語句,因爲你以後不會再用它了。 轉而導入 HeroService。
src/app/heroes/heroes.component.ts (import HeroService)
content_copyimport { HeroService } from '../hero.service';
把 heroes 屬性的定義改爲一句簡單的聲明。
src/app/heroes/heroes.component.ts
content_copyheroes: Hero[];
注入 HeroService
往構造函數中添加一個私有的 heroService,其類型爲 HeroService。
src/app/heroes/heroes.component.ts
content_copyconstructor(private heroService: HeroService) {}
這個參數同時做了兩件事:1. 聲明瞭一個私有 heroService 屬性,2. 把它標記爲一個 HeroService 的注入點。
當 Angular 創建 HeroesComponent 時,依賴注入系統就會把這個 heroService 參數設置爲 HeroService 的單例對象。
添加 getHeroes()
創建一個方法,以從服務中獲取這些英雄數據。
src/app/heroes/heroes.component.ts
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}
在 ngOnInit() 中調用它
你固然可以在構造函數中調用 getHeroes(),但那不是最佳實踐。
讓構造函數保持簡單,只做初始化操作,比如把構造函數的參數賦值給屬性。 構造函數不應該做任何事。 它當然不應該調用某個函數來向遠端服務(比如真實的數據服務)發起 HTTP 請求。
而是選擇在 ngOnInit 生命週期鉤子中調用 getHeroes(),之後 Angular 會在構造出 HeroesComponent 的實例之後的某個合適的時機調用 ngOnInit()。
src/app/heroes/heroes.component.ts
ngOnInit() {
this.getHeroes();
}
可觀察對象版本的 HeroService
Observable 是 RxJS 庫中的一個關鍵類。
在稍後的 HTTP 教程中,你就會知道 Angular HttpClient 的方法會返回 RxJS 的 Observable。 這節課,你將使用 RxJS 的 of() 函數來模擬從服務器返回數據。
打開 HeroService 文件,並從 RxJS 中導入 Observable 和 of 符號。
src/app/hero.service.ts (Observable imports)
import { Observable, of } from 'rxjs';
把 getHeroes() 方法改成這樣:
src/app/hero.service.ts
getHeroes(): Observable<Hero[]> {
return of(HEROES);
}
of(HEROES) 會返回一個 Observable<Hero[]>,它會發出單個值,這個值就是這些模擬英雄的數組。
在 HTTP 教程中,你將會調用 HttpClient.get<Hero[]>() 它也同樣返回一個 Observable<Hero[]>,它也會發出單個值,這個值就是來自 HTTP 響應體中的英雄數組。
在 HeroesComponent 中訂閱
HeroService.getHeroes 方法之前返回一個 Hero[], 現在它返回的是 Observable<Hero[]>。
你必須在 HeroesComponent 中也向本服務中的這種形式看齊。
找到 getHeroes 方法,並且把它替換爲如下代碼(和前一個版本對比顯示):
heroes.component.ts (Observable)
content_copygetHeroes(): 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。
content_copyng generate component messages
CLI 在 src/app/messages 中創建了組件文件,並且把 MessagesComponent 聲明在了 AppModule 中。
修改 AppComponent 的模板來顯示所生成的 MessagesComponent:
src/app/app.component.html
content_copy<h1>{{title}}</h1>
<app-heroes></app-heroes>
<app-messages></app-messages>
你可以在頁面的底部看到來自的 MessagesComponent 的默認內容。
創建 MessageService
使用 CLI 在 src/app 中創建 MessageService。
content_copyng generate service message
打開 MessageService,並把它的內容改成這樣:
src/app/message.service.ts
content_copyimport { 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。
src/app/hero.service.ts (import MessageService)
content_copyimport { MessageService } from './message.service';
修改這個構造函數,添加一個私有的 messageService 屬性參數。 Angular 將會在創建 HeroService 時把 MessageService 的單例注入到這個屬性中。
src/app/hero.service.ts
content_copyconstructor(private messageService: MessageService) { }
這是一個典型的“服務中的服務”場景: 你把 MessageService 注入到了 HeroService 中,而 HeroService 又被注入到了 HeroesComponent 中。
從 HeroService 中發送一條消息
修改 getHeroes() 方法,在獲取到英雄數組時發送一條消息。
src/app/hero.service.ts
content_copygetHeroes(): Observable<Hero[]> {
// TODO: send the message _after_ fetching the heroes
this.messageService.add('HeroService: fetched heroes');
return of(HEROES);
}
從 HeroService 中顯示消息
MessagesComponent 可以顯示所有消息, 包括當 HeroService 獲取到英雄數據時發送的那條。
打開 MessagesComponent,並且導入 MessageService。
src/app/messages/messages.component.ts (import MessageService)
content_copyimport { MessageService } from '../message.service';
修改構造函數,添加一個 public 的 messageService 屬性。 Angular 將會在創建 MessagesComponent 的實例時 把 MessageService 的實例注入到這個屬性中。
src/app/messages/messages.component.ts
content_copyconstructor(public messageService: MessageService) {}
這個 messageService 屬性必須是公共屬性,因爲你將會在模板中綁定到它。
Angular 只會綁定到組件的公共屬性。
綁定到 MessageService
把 CLI 生成的 MessagesComponent 的模板改成這樣:
src/app/messages/messages.component.html
content_copy<div *ngIf="messageService.messages.length">
<h2>Messages</h2>
<button class="clear"
(click)="messageService.clear()">clear</button>
<div *ngFor='let message of messageService.messages'> {{message}} </div>
</div>
這個模板直接綁定到了組件的 messageService 屬性上。
• *ngIf 只有在有消息時纔會顯示消息區。
• *ngFor 用來在一系列 <div> 元素中展示消息列表。
• Angular 的事件綁定把按鈕的 click 事件綁定到了 MessageService.clear()。
當你把 最終代碼 某一頁的內容添加到 messages.component.css 中時,這些消息會變得好看一些。
爲 hero 服務添加額外的消息
下面的例子展示了當用戶點擊某個英雄時,如何發送和顯示一條消息,以及如何顯示該用戶的選取歷史。當你學到後面的路由一章時,這會很有幫助。
src/app/heroes/heroes.component.ts
content_copyimport { 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() {
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 的消息。 點擊“清空”按鈕,消息區不見了。
查看最終代碼
小結
• 你把數據訪問邏輯重構到了 HeroService 類中。
• 你在根注入器中把 HeroService 註冊爲該服務的提供者,以便在別處可以注入它。
• 你使用 Angular 依賴注入機制把它注入到了組件中。
• 你給 HeroService 中獲取數據的方法提供了一個異步的函數簽名。
• 你發現了 Observable 以及 RxJS 庫。
• 你使用 RxJS 的 of() 方法返回了一個模擬英雄數據的可觀察對象 (Observable<Hero[]>)。
• 在組件的 ngOnInit 生命週期鉤子中調用 HeroService 方法,而不是構造函數中。
• 你創建了一個 MessageService,以便在類之間實現松耦合通訊。
• HeroService 連同注入到它的服務 MessageService 一起,注入到了組件中。