英雄指南的 HeroesComponent
目前獲取和顯示的都是模擬數(shù)據(jù)。
本節(jié)課的重構(gòu)完成之后,HeroesComponent
變得更精簡(jiǎn),并且聚焦于為它的視圖提供支持。這也讓它更容易使用模擬服務(wù)進(jìn)行單元測(cè)試。
組件不應(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è)地方:
HeroService
中,它會(huì)使用該服務(wù)發(fā)送消息MessagesComponent
中,它會(huì)顯示其中的消息。當(dāng)用戶點(diǎn)擊某個(gè)英雄時(shí),它還會(huì)顯示該英雄的 ID
。
使用 Angular CLI 創(chuàng)建一個(gè)名叫 hero
的服務(wù)。
ng generate service hero
該命令會(huì)在 "src/app/hero.service.ts" 中生成 HeroService
類的骨架,代碼如下:
Path:"src/app/hero.service.ts (new service)"
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class HeroService {
constructor() { }
}
注意,這個(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ì)組件類的作用一樣。
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"
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
添加一個(gè) getHeroes
方法,讓它返回模擬的英雄列表。
Path:"src/app/hero.service.ts"
getHeroes(): Hero[] {
return HEROES;
}
你必須先注冊(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è)成為提供者。
@Injectable({
providedIn: 'root',
})
注:
- 這是一個(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
類文件。
刪除 HEROES
的導(dǎo)入語(yǔ)句,因?yàn)槟阋院蟛粫?huì)再用它了。 轉(zhuǎn)而導(dǎo)入 HeroService
。
Path:"src/app/heroes/heroes.component.ts (import HeroService)"
import { HeroService } from '../hero.service';
把 heroes 屬性的定義改為一句簡(jiǎn)單的聲明。
Path:"src/app/heroes/heroes.component.ts"
heroes: Hero[];
往構(gòu)造函數(shù)中添加一個(gè)私有的 heroService
,其類型為 HeroService
。
Path:"src/app/heroes/heroes.component.ts"
constructor(private heroService: HeroService) {}
這個(gè)參數(shù)同時(shí)做了兩件事:
heroService
屬性。HeroService
的注入點(diǎn)。
當(dāng) Angular 創(chuàng)建 HeroesComponent
時(shí),依賴注入系統(tǒng)就會(huì)把這個(gè) heroService
參數(shù)設(shè)置為 HeroService
的單例對(duì)象。
創(chuàng)建一個(gè)方法,以從服務(wù)中獲取這些英雄數(shù)據(jù)。
Path:"src/app/heroes/heroes.component.ts"
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}
你固然可以在構(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"
getHeroes(): void {
ngOnInit() {
this.getHeroes();
}
刷新瀏覽器,該應(yīng)用仍運(yùn)行的一如既往。 顯示英雄列表,并且當(dāng)你點(diǎn)擊某個(gè)英雄的名字時(shí)顯示出英雄詳情視圖。
HeroService.getHeroes()
的函數(shù)簽名是同步的,它所隱含的假設(shè)是 HeroService
總是能同步獲取英雄列表數(shù)據(jù)。 而 HeroesComponent
也同樣假設(shè)能同步取到 getHeroes()
的結(jié)果。
Path:"src/app/heroes/heroes.component.ts"
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
。
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)入 Observable
和 of
符號(hào)。
Path:"src/app/hero.service.ts (Observable imports)"
import { Observable, of } from 'rxjs';
把 getHeroes()
方法改成這樣:
Path:"src/app/hero.service.ts"
getHeroes(): Observable<Hero[]> {
return of(HEROES);
}
of(HEROES)
會(huì)返回一個(gè) Observable<Hero[]>
,它會(huì)發(fā)出單個(gè)值,這個(gè)值就是這些模擬英雄的數(shù)組。
HeroService.getHeroes
方法之前返回一個(gè) Hero[]
, 現(xiàn)在它返回的是 Observable<Hero[]>
。
你必須在 HeroesComponent
中也向本服務(wù)中的這種形式看齊。
找到 getHeroes
方法,并且把它替換為如下代碼(和前一個(gè)版本對(duì)比顯示):
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}
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)你:
MessagesComponent
,它在屏幕的底部顯示應(yīng)用中的消息。MessageService
,用于發(fā)送要顯示的消息。MessageService
注入到 HeroService
中。HeroService
成功獲取了英雄數(shù)據(jù)時(shí)顯示一條消息。使用 CLI 創(chuàng)建 MessagesComponent。
ng generate component messages
CLI 在 "src/app/messages" 中創(chuàng)建了組件文件,并且把 MessagesComponent
聲明在了 AppModule
中。
修改 AppComponent
的模板來(lái)顯示所生成的 MessagesComponent
:
Path:"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 = [];
}
}
該服務(wù)對(duì)外暴露了它的 messages
緩存,以及兩個(gè)方法:add()
方法往緩存中添加一條消息,clear()
方法用于清空緩存。
在 HeroService 中導(dǎo)入 MessageService。
Path:"src/app/hero.service.ts (import MessageService)"
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"
constructor(private messageService: MessageService) { }
注:
- 這是一個(gè)典型的“服務(wù)中的服務(wù)”場(chǎng)景: 你把MessageService
注入到了HeroService
中,而HeroService
又被注入到了HeroesComponent
中。
修改 getHeroes()
方法,在獲取到英雄數(shù)組時(shí)發(fā)送一條消息。
Path:"src/app/hero.service.ts"
getHeroes(): Observable<Hero[]> {
// TODO: send the message _after_ fetching the heroes
this.messageService.add('HeroService: fetched heroes');
return of(HEROES);
}
MessagesComponent
可以顯示所有消息, 包括當(dāng) HeroService
獲取到英雄數(shù)據(jù)時(shí)發(fā)送的那條。
打開 MessagesComponent
,并且導(dǎo)入 MessageService
。
Path:"src/app/messages/messages.component.ts (import MessageService)"
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"
constructor(public messageService: MessageService) {}
這個(gè) messageService
屬性必須是公共屬性,因?yàn)槟銓?huì)在模板中綁定到它。
把 CLI 生成的 MessagesComponent 的模板改成這樣:
Path:"src/app/messages/messages.component.html"
<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>
這個(gè)模板直接綁定到了組件的 messageService
屬性上。
*ngIf
只有在有消息時(shí)才會(huì)顯示消息區(qū)。*ngFor
用來(lái)在一系列 <div>
元素中展示消息列表。click
事件綁定到了 MessageService.clear()
。
當(dāng)你把 最終代碼 某一頁(yè)的內(nèi)容添加到 messages.component.css
中時(shí),這些消息會(huì)變得好看一些。
下面的例子展示了當(dāng)用戶點(diǎn)擊某個(gè)英雄時(shí),如何發(fā)送和顯示一條消息,以及如何顯示該用戶的選取歷史。當(dāng)你學(xué)到后面的路由一章時(shí),這會(huì)很有幫助。
Path:"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() {
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);
}
}
刷新瀏覽器,頁(yè)面顯示出了英雄列表。 滾動(dòng)到底部,就會(huì)在消息區(qū)看到來(lái)自 HeroService
的消息。 點(diǎn)擊“清空”按鈕,消息區(qū)不見了。
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[]> {
// TODO: send the message _after_ fetching the heroes
this.messageService.add('HeroService: fetched heroes');
return of(HEROES);
}
}
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}
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() {
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);
}
}
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() {
}
}
<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>
/* MessagesComponent's private CSS styles */
h2 {
color: red;
font-family: Arial, Helvetica, sans-serif;
font-weight: lighter;
}
body {
margin: 2em;
}
body, input[text], button {
color: crimson;
font-family: Cambria, Georgia;
}
button.clear {
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #aaa;
cursor: auto;
}
button.clear {
color: #333;
margin-bottom: 12px;
}
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 { }
<h1>{{title}}</h1>
<app-heroes></app-heroes>
<app-messages></app-messages>
更多建議: