英雄指南的 HeroesComponent
目前獲取和顯示的都是模擬數(shù)據(jù)。
本節(jié)課的重構完成之后,HeroesComponent
變得更精簡,并且聚焦于為它的視圖提供支持。這也讓它更容易使用模擬服務進行單元測試。
組件不應該直接獲取或保存數(shù)據(jù),它們不應該了解是否在展示假數(shù)據(jù)。 它們應該聚焦于展示數(shù)據(jù),而把數(shù)據(jù)訪問的職責委托給某個服務。
本節(jié)課,你將創(chuàng)建一個 HeroService
,應用中的所有類都可以使用它來獲取英雄列表。 不要使用 new
關鍵字來創(chuàng)建此服務,而要依靠 Angular 的依賴注入機制把它注入到 HeroesComponent
的構造函數(shù)中。
服務是在多個“互相不知道”的類之間共享信息的好辦法。 你將創(chuàng)建一個 MessageService
,并且把它注入到兩個地方:
HeroService
中,它會使用該服務發(fā)送消息MessagesComponent
中,它會顯示其中的消息。當用戶點擊某個英雄時,它還會顯示該英雄的 ID
。
使用 Angular CLI 創(chuàng)建一個名叫 hero
的服務。
ng generate service hero
該命令會在 "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() { }
}
注意,這個新的服務導入了 Angular 的 Injectable
符號,并且給這個服務類添加了 @Injectable()
裝飾器。 它把這個類標記為依賴注入系統(tǒng)的參與者之一。HeroService
類將會提供一個可注入的服務,并且它還可以擁有自己的待注入的依賴。 目前它還沒有依賴,但是很快就會有了。
@Injectable()
裝飾器會接受該服務的元數(shù)據(jù)對象,就像 @Component()
對組件類的作用一樣。
HeroService
可以從任何地方獲取數(shù)據(jù):Web 服務、本地存儲(LocalStorage
)或一個模擬的數(shù)據(jù)源。
從組件中移除數(shù)據(jù)訪問邏輯,意味著將來任何時候你都可以改變目前的實現(xiàn)方式,而不用改動任何組件。 這些組件不需要了解該服務的內部實現(xiàn)。
這節(jié)課中的實現(xiàn)仍然會提供模擬的英雄列表。
導入 Hero 和 HEROES。
Path:"src/app/hero.service.ts"
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
添加一個 getHeroes
方法,讓它返回模擬的英雄列表。
Path:"src/app/hero.service.ts"
getHeroes(): Hero[] {
return HEROES;
}
你必須先注冊一個服務提供者,來讓 HeroService
在依賴注入系統(tǒng)中可用,Angular 才能把它注入到 HeroesComponent
中。所謂服務提供者就是某種可用來創(chuàng)建或交付一個服務的東西;在這里,它通過實例化 HeroService
類,來提供該服務。
為了確保 HeroService
可以提供該服務,就要使用注入器來注冊它。注入器是一個對象,負責當應用要求獲取它的實例時選擇和注入該提供者。
默認情況下,Angular CLI 命令 ng generate service
會通過給 @Injectable()
裝飾器添加 providedIn: 'root'
元數(shù)據(jù)的形式,用根注入器將你的服務注冊成為提供者。
@Injectable({
providedIn: 'root',
})
注:
- 這是一個過渡性的代碼范例,它將會允許你提供并使用 HeroService。此刻的代碼和最終代碼相差很大。
當你在頂層提供該服務時,Angular 就會為 HeroService
創(chuàng)建一個單一的、共享的實例,并把它注入到任何想要它的類上。 在 @Injectable
元數(shù)據(jù)中注冊該提供者,還能允許 Angular 通過移除那些完全沒有用過的服務來進行優(yōu)化。
現(xiàn)在 HeroService
已經準備好插入到 HeroesComponent
中了。
打開 HeroesComponent
類文件。
刪除 HEROES
的導入語句,因為你以后不會再用它了。 轉而導入 HeroService
。
Path:"src/app/heroes/heroes.component.ts (import HeroService)"
import { HeroService } from '../hero.service';
把 heroes 屬性的定義改為一句簡單的聲明。
Path:"src/app/heroes/heroes.component.ts"
heroes: Hero[];
往構造函數(shù)中添加一個私有的 heroService
,其類型為 HeroService
。
Path:"src/app/heroes/heroes.component.ts"
constructor(private heroService: HeroService) {}
這個參數(shù)同時做了兩件事:
heroService
屬性。HeroService
的注入點。
當 Angular 創(chuàng)建 HeroesComponent
時,依賴注入系統(tǒng)就會把這個 heroService
參數(shù)設置為 HeroService
的單例對象。
創(chuàng)建一個方法,以從服務中獲取這些英雄數(shù)據(jù)。
Path:"src/app/heroes/heroes.component.ts"
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}
你固然可以在構造函數(shù)中調用 getHeroes()
,但那不是最佳實踐。
讓構造函數(shù)保持簡單,只做初始化操作,比如把構造函數(shù)的參數(shù)賦值給屬性。 構造函數(shù)不應該做任何事。 它當然不應該調用某個函數(shù)來向遠端服務(比如真實的數(shù)據(jù)服務)發(fā)起 HTTP 請求。
而是選擇在 ngOnInit
生命周期鉤子中調用 getHeroes()
,之后 Angular 會在構造出 HeroesComponent
的實例之后的某個合適的時機調用 ngOnInit()
。
Path:"src/app/heroes/heroes.component.ts"
getHeroes(): void {
ngOnInit() {
this.getHeroes();
}
刷新瀏覽器,該應用仍運行的一如既往。 顯示英雄列表,并且當你點擊某個英雄的名字時顯示出英雄詳情視圖。
HeroService.getHeroes()
的函數(shù)簽名是同步的,它所隱含的假設是 HeroService
總是能同步獲取英雄列表數(shù)據(jù)。 而 HeroesComponent
也同樣假設能同步取到 getHeroes()
的結果。
Path:"src/app/heroes/heroes.component.ts"
this.heroes = this.heroService.getHeroes();
這在真實的應用中幾乎是不可能的。 現(xiàn)在能這么做,只是因為目前該服務返回的是模擬數(shù)據(jù)。 不過很快,該應用就要從遠端服務器獲取英雄數(shù)據(jù)了,而那天生就是異步操作。
HeroService
必須等服務器給出響應, 而 getHeroes()
不能立即返回英雄數(shù)據(jù), 瀏覽器也不會在該服務等待期間停止響應。
HeroService.getHeroes()
必須具有某種形式的異步函數(shù)簽名。
這節(jié)課,HeroService.getHeroes()
將會返回 Observable
,部分原因在于它最終會使用 Angular 的 HttpClient.get()
方法來獲取英雄數(shù)據(jù),而 HttpClient.get()
會返回 Observable
。
Observable
是 RxJS 庫中的一個關鍵類。
在稍后的 HTTP 教程中,你就會知道 Angular HttpClient
的方法會返回 RxJS 的 Observable
。 這節(jié)課,你將使用 RxJS 的 of()
函數(shù)來模擬從服務器返回數(shù)據(jù)。
打開 "HeroService" 文件,并從 RxJS 中導入 Observable
和 of
符號。
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)
會返回一個 Observable<Hero[]>
,它會發(fā)出單個值,這個值就是這些模擬英雄的數(shù)組。
HeroService.getHeroes
方法之前返回一個 Hero[]
, 現(xiàn)在它返回的是 Observable<Hero[]>
。
你必須在 HeroesComponent
中也向本服務中的這種形式看齊。
找到 getHeroes
方法,并且把它替換為如下代碼(和前一個版本對比顯示):
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}
Observable.subscribe()
是關鍵的差異點。
上一個版本把英雄的數(shù)組賦值給了該組件的 heroes
屬性。 這種賦值是同步的,這里包含的假設是服務器能立即返回英雄數(shù)組或者瀏覽器能在等待服務器響應時凍結界面。
當 HeroService
真的向遠端服務器發(fā)起請求時,這種方式就行不通了。
新的版本等待 Observable
發(fā)出這個英雄數(shù)組,這可能立即發(fā)生,也可能會在幾分鐘之后。 然后,subscribe()
方法把這個英雄數(shù)組傳給這個回調函數(shù),該函數(shù)把英雄數(shù)組賦值給組件的 heroes
屬性。
使用這種異步方式,當 HeroService
從遠端服務器獲取英雄數(shù)據(jù)時,就可以工作了。
這一節(jié)將指導你:
MessagesComponent
,它在屏幕的底部顯示應用中的消息。MessageService
,用于發(fā)送要顯示的消息。MessageService
注入到 HeroService
中。HeroService
成功獲取了英雄數(shù)據(jù)時顯示一條消息。使用 CLI 創(chuàng)建 MessagesComponent。
ng generate component messages
CLI 在 "src/app/messages" 中創(chuàng)建了組件文件,并且把 MessagesComponent
聲明在了 AppModule
中。
修改 AppComponent
的模板來顯示所生成的 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 = [];
}
}
該服務對外暴露了它的 messages
緩存,以及兩個方法:add()
方法往緩存中添加一條消息,clear()
方法用于清空緩存。
在 HeroService 中導入 MessageService。
Path:"src/app/hero.service.ts (import MessageService)"
import { MessageService } from './message.service';
修改這個構造函數(shù),添加一個私有的 messageService
屬性參數(shù)。 Angular 將會在創(chuàng)建 HeroService
時把 MessageService
的單例注入到這個屬性中。
Path:"src/app/hero.service.ts"
constructor(private messageService: MessageService) { }
注:
- 這是一個典型的“服務中的服務”場景: 你把MessageService
注入到了HeroService
中,而HeroService
又被注入到了HeroesComponent
中。
修改 getHeroes()
方法,在獲取到英雄數(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
可以顯示所有消息, 包括當 HeroService
獲取到英雄數(shù)據(jù)時發(fā)送的那條。
打開 MessagesComponent
,并且導入 MessageService
。
Path:"src/app/messages/messages.component.ts (import MessageService)"
import { MessageService } from '../message.service';
修改構造函數(shù),添加一個 public 的 messageService
屬性。 Angular 將會在創(chuàng)建 MessagesComponent
的實例時 把 MessageService
的實例注入到這個屬性中。
Path:"src/app/messages/messages.component.ts"
constructor(public messageService: MessageService) {}
這個 messageService
屬性必須是公共屬性,因為你將會在模板中綁定到它。
把 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>
這個模板直接綁定到了組件的 messageService
屬性上。
*ngIf
只有在有消息時才會顯示消息區(qū)。*ngFor
用來在一系列 <div>
元素中展示消息列表。click
事件綁定到了 MessageService.clear()
。
當你把 最終代碼 某一頁的內容添加到 messages.component.css
中時,這些消息會變得好看一些。
下面的例子展示了當用戶點擊某個英雄時,如何發(fā)送和顯示一條消息,以及如何顯示該用戶的選取歷史。當你學到后面的路由一章時,這會很有幫助。
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);
}
}
刷新瀏覽器,頁面顯示出了英雄列表。 滾動到底部,就會在消息區(qū)看到來自 HeroService
的消息。 點擊“清空”按鈕,消息區(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>
更多建議: