服務
Add services
英雄之旅的 HeroesComponent
目前獲取和顯示的都是模擬資料。
The Tour of Heroes HeroesComponent
is currently getting and displaying fake data.
本節課的重構完成之後,HeroesComponent
變得更精簡,並且聚焦於為它的檢視提供支援。這也讓它更容易使用模擬服務進行單元測試。
After the refactoring in this tutorial, HeroesComponent
will be lean and focused on supporting the view. It will also be easier to unit-test with a mock service.
為什麼需要服務
Why services
元件不應該直接獲取或儲存資料,它們不應該瞭解是否在展示假資料。 它們應該聚焦於展示資料,而把資料訪問的職責委託給某個服務。
Components shouldn't fetch or save data directly and they certainly shouldn't knowingly present fake data. They should focus on presenting data and delegate data access to a service.
本節課,你將建立一個 HeroService
,應用中的所有類別都可以使用它來獲取英雄列表。 不要使用 new
關鍵字來建立此服務,而要依靠 Angular 的依賴注入機制把它注入到 HeroesComponent
的建構函式中。
In this tutorial, you'll create a HeroService
that all application classes can use to get heroes. Instead of creating that service with the new
keyword, you'll rely on Angular dependency injection to inject it into the HeroesComponent
constructor.
服務是在多個“互相不知道”的類別之間共享資訊的好辦法。 你將建立一個 MessageService
,並且把它注入到兩個地方:
Services are a great way to share information among classes that don't know each other. You'll create a MessageService
and inject it in two places.
注入到
HeroService
中,它會使用該服務傳送訊息Inject in HeroService, which uses the service to send a message.
注入到
MessagesComponent
中,它會顯示其中的訊息。當用戶點選某個英雄時,它還會顯示該英雄的 ID。Inject in MessagesComponent, which displays that message, and also displays the ID when the user clicks a hero.
建立 HeroService
Create the HeroService
使用 Angular CLI 建立一個名叫 hero
的服務。
Using the Angular CLI, create a service called hero
.
ng generate service hero
該命令會在 src/app/hero.service.ts
中產生 HeroService
類別的骨架,程式碼如下:
The command generates a skeleton HeroService
class in src/app/hero.service.ts
as follows:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class HeroService {
constructor() { }
}
@Injectable()
服務
@Injectable()
services
注意,這個新的服務匯入了 Angular 的 Injectable
符號,並且給這個服務類別添加了 @Injectable()
裝飾器。 它把這個類別標記為依賴注入系統的參與者之一。HeroService
類別將會提供一個可注入的服務,並且它還可以擁有自己的待注入的依賴。 目前它還沒有依賴,但是很快就會有了。
Notice that the new service imports the Angular Injectable
symbol and annotates the class with the @Injectable()
decorator. This marks the class as one that participates in the dependency injection system. The HeroService
class is going to provide an injectable service, and it can also have its own injected dependencies. It doesn't have any dependencies yet, but it will soon.
@Injectable()
裝飾器會接受該服務的元資料物件,就像 @Component()
對元件類別的作用一樣。
The @Injectable()
decorator accepts a metadata object for the service, the same way the @Component()
decorator did for your component classes.
獲取英雄資料
Get hero data
HeroService
可以從任何地方獲取資料:Web 服務、本地儲存(LocalStorage)或一個模擬的資料來源。
The HeroService
could get hero data from anywhere—a web service, local storage, or a mock data source.
從元件中移除資料訪問邏輯,意味著將來任何時候你都可以改變目前的實現方式,而不用改動任何元件。 這些元件不需要了解該服務的內部實現。
Removing data access from components means you can change your mind about the implementation anytime, without touching any components. They don't know how the service works.
這節課中的實現仍然會提供模擬的英雄列表。
The implementation in this tutorial will continue to deliver mock heroes.
匯入 Hero
和 HEROES
。
Import the Hero
and HEROES
.
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
新增一個 getHeroes
方法,讓它返回模擬的英雄列表。
Add a getHeroes
method to return the mock heroes.
getHeroes(): Hero[] {
return HEROES;
}
提供(provide) HeroService
Provide the HeroService
你必須先註冊一個服務提供者,來讓 HeroService
在依賴注入系統中可用,Angular 才能把它注入到 HeroesComponent
中。所謂服務提供者就是某種可用來建立或交付一個服務的東西;在這裡,它透過實例化 HeroService
類別,來提供該服務。
You must make the HeroService
available to the dependency injection system before Angular can inject it into the HeroesComponent
by registering a provider. A provider is something that can create or deliver a service; in this case, it instantiates the HeroService
class to provide the service.
為了確保 HeroService
可以提供該服務,就要使用注入器來註冊它。注入器是一個物件,負責當應用要求獲取它的實例時選擇和注入該提供者。
To make sure that the HeroService
can provide this service, register it with the injector, which is the object that is responsible for choosing and injecting the provider where the app requires it.
預設情況下,Angular CLI 命令 ng generate service
會透過給 @Injectable()
裝飾器新增 providedIn: 'root'
元資料的形式,用根注入器將你的服務註冊成為提供者。
By default, the Angular CLI command ng generate service
registers a provider with the root injector for your service by including provider metadata, that is providedIn: 'root'
in the @Injectable()
decorator.
@Injectable({
providedIn: 'root',
})
當你在最上層提供該服務時,Angular 就會為 HeroService
建立一個單一的、共享的實例,並把它注入到任何想要它的類別上。 在 @Injectable
元資料中註冊該提供者,還能允許 Angular 透過移除那些完全沒有用過的服務來進行優化。
When you provide the service at the root level, Angular creates a single, shared instance of HeroService
and injects into any class that asks for it. Registering the provider in the @Injectable
metadata also allows Angular to optimize an app by removing the service if it turns out not to be used after all.
要了解關於提供者的更多知識,參閱提供者部分。 要了解關於注入器的更多知識,參閱依賴注入指南。
To learn more about providers, see the Providers section. To learn more about injectors, see the Dependency Injection guide.
現在 HeroService
已經準備好插入到 HeroesComponent
中了。
The HeroService
is now ready to plug into the HeroesComponent
.
這是一個過渡性的程式碼範例,它將會允許你提供並使用 HeroService
。此刻的程式碼和最終程式碼相差很大。
This is an interim code sample that will allow you to provide and use the HeroService
. At this point, the code will differ from the HeroService
in the "final code review".
修改 HeroesComponent
Update HeroesComponent
開啟 HeroesComponent
類別檔案。
Open the HeroesComponent
class file.
刪除 HEROES
的匯入語句,因為你以後不會再用它了。 轉而匯入 HeroService
。
Delete the HEROES
import, because you won't need that anymore. Import the HeroService
instead.
import { HeroService } from '../hero.service';
把 heroes
屬性的定義改為一句簡單的宣告。
Replace the definition of the heroes
property with a simple declaration.
heroes: Hero[];
注入 HeroService
Inject the HeroService
往建構函式中新增一個私有的 heroService
,其型別為 HeroService
。
Add a private heroService
parameter of type HeroService
to the constructor.
constructor(private heroService: HeroService) {}
這個引數同時做了兩件事:1. 聲明瞭一個私有 heroService
屬性,2. 把它標記為一個 HeroService
的注入點。
The parameter simultaneously defines a private heroService
property and identifies it as a HeroService
injection site.
當 Angular 建立 HeroesComponent
時,依賴注入系統就會把這個 heroService
引數設定為 HeroService
的單例物件。
When Angular creates a HeroesComponent
, the Dependency Injection system sets the heroService
parameter to the singleton instance of HeroService
.
新增 getHeroes()
Add getHeroes()
建立一個方法,以從服務中獲取這些英雄資料。
Create a method to retrieve the heroes from the service.
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}
在 ngOnInit()
中呼叫它
Call it in ngOnInit()
你固然可以在建構函式中呼叫 getHeroes()
,但那不是最佳實踐。
While you could call getHeroes()
in the constructor, that's not the best practice.
讓建構函式保持簡單,只做初始化操作,比如把建構函式的引數賦值給屬性。 建構函式不應該做任何事。 它當然不應該呼叫某個函式來向遠端服務(比如真實的資料服務)發起 HTTP 請求。
Reserve the constructor for simple initialization such as wiring constructor parameters to properties. The constructor shouldn't do anything. It certainly shouldn't call a function that makes HTTP requests to a remote server as a real data service would.
而是選擇在 ngOnInit 生命週期鉤子中呼叫 getHeroes()
,之後 Angular 會在構造出 HeroesComponent
的實例之後的某個合適的時機呼叫 ngOnInit()
。
Instead, call getHeroes()
inside the ngOnInit lifecycle hook and let Angular call ngOnInit()
at an appropriate time after constructing a HeroesComponent
instance.
ngOnInit() {
this.getHeroes();
}
檢視執行效果
See it run
重新整理瀏覽器,該應用仍執行的一如既往。 顯示英雄列表,並且當你點選某個英雄的名字時顯示出英雄詳情檢視。
After the browser refreshes, the app should run as before, showing a list of heroes and a hero detail view when you click on a hero name.
可觀察(Observable)的資料
Observable data
HeroService.getHeroes()
的函式簽名是同步的,它所隱含的假設是 HeroService
總是能同步獲取英雄列表資料。 而 HeroesComponent
也同樣假設能同步取到 getHeroes()
的結果。
The HeroService.getHeroes()
method has a synchronous signature, which implies that the HeroService
can fetch heroes synchronously. The HeroesComponent
consumes the getHeroes()
result as if heroes could be fetched synchronously.
this.heroes = this.heroService.getHeroes();
這在真實的應用中幾乎是不可能的。 現在能這麼做,只是因為目前該服務返回的是模擬資料。 不過很快,該應用就要從遠端伺服器獲取英雄資料了,而那天生就是非同步操作。
This will not work in a real app. You're getting away with it now because the service currently returns mock heroes. But soon the app will fetch heroes from a remote server, which is an inherently asynchronous operation.
HeroService
必須等伺服器給出響應, 而 getHeroes()
不能立即返回英雄資料, 瀏覽器也不會在該服務等待期間停止響應。
The HeroService
must wait for the server to respond, getHeroes()
cannot return immediately with hero data, and the browser will not block while the service waits.
HeroService.getHeroes()
必須具有某種形式的非同步函式簽名。
HeroService.getHeroes()
must have an asynchronous signature of some kind.
這節課,HeroService.getHeroes()
將會返回 Observable
,部分原因在於它最終會使用 Angular 的 HttpClient.get
方法來獲取英雄資料,而 HttpClient.get()
會返回 Observable
。
In this tutorial, HeroService.getHeroes()
will return an Observable
because it will eventually use the Angular HttpClient.get
method to fetch the heroes and HttpClient.get()
returns an Observable
.
可觀察物件版本的 HeroService
Observable HeroService
Observable
是 RxJS 函式庫中的一個關鍵類別。
Observable
is one of the key classes in the RxJS library.
在稍後的 HTTP 課程中,你就會知道 Angular HttpClient
的方法會返回 RxJS 的 Observable
。 這節課,你將使用 RxJS 的 of()
函式來模擬從伺服器返回資料。
In a later tutorial on HTTP, you'll learn that Angular's HttpClient
methods return RxJS Observable
s. In this tutorial, you'll simulate getting data from the server with the RxJS of()
function.
開啟 HeroService
檔案,並從 RxJS 中匯入 Observable
和 of
符號。
Open the HeroService
file and import the Observable
and of
symbols from RxJS.
import { Observable, of } from 'rxjs';
把 getHeroes()
方法改成這樣:
Replace the getHeroes()
method with the following:
getHeroes(): Observable<Hero[]> {
return of(HEROES);
}
of(HEROES)
會返回一個 Observable<Hero[]>
,它會發出單個值,這個值就是這些模擬英雄的陣列。
of(HEROES)
returns an Observable<Hero[]>
that emits a single value, the array of mock heroes.
在 HTTP 課程中,你將會呼叫 HttpClient.get<Hero[]>()
它也同樣返回一個 Observable<Hero[]>
,它也會發出單個值,這個值就是來自 HTTP 回應內文中的英雄陣列。
In the HTTP tutorial, you'll call HttpClient.get<Hero[]>()
which also returns an Observable<Hero[]>
that emits a single value, an array of heroes from the body of the HTTP response.
在 HeroesComponent
中訂閱
Subscribe in HeroesComponent
HeroService.getHeroes
方法之前返回一個 Hero[]
, 現在它返回的是 Observable<Hero[]>
。
The HeroService.getHeroes
method used to return a Hero[]
. Now it returns an Observable<Hero[]>
.
你必須在 HeroesComponent
中也向本服務中的這種形式看齊。
You'll have to adjust to that difference in HeroesComponent
.
找到 getHeroes
方法,並且把它替換為如下程式碼(和前一個版本對比顯示):
Find the getHeroes
method and replace it with the following code (shown side-by-side with the previous version for comparison)
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
Observable.subscribe()
是關鍵的差異點。
Observable.subscribe()
is the critical difference.
上一個版本把英雄的陣列賦值給了該元件的 heroes
屬性。 這種賦值是同步的,這裡包含的假設是伺服器能立即返回英雄陣列或者瀏覽器能在等待伺服器響應時凍結介面。
The previous version assigns an array of heroes to the component's heroes
property. The assignment occurs synchronously, as if the server could return heroes instantly or the browser could freeze the UI while it waited for the server's response.
當 HeroService
真的向遠端伺服器發起請求時,這種方式就行不通了。
That won't work when the HeroService
is actually making requests of a remote server.
新的版本等待 Observable
發出這個英雄陣列,這可能立即發生,也可能會在幾分鐘之後。 然後,subscribe()
方法把這個英雄陣列傳給這個回呼(Callback)函式,該函式把英雄陣列賦值給元件的 heroes
屬性。
The new version waits for the Observable
to emit the array of heroes—which could happen now or several minutes from now. The subscribe()
method passes the emitted array to the callback, which sets the component's heroes
property.
使用這種非同步方式,當 HeroService
從遠端伺服器獲取英雄資料時,就可以工作了。
This asynchronous approach will work when the HeroService
requests heroes from the server.
顯示訊息
Show messages
這一節將指導你:
This section guides you through the following:
新增一個
MessagesComponent
,它在螢幕的底部顯示應用中的訊息。adding a
MessagesComponent
that displays app messages at the bottom of the screen建立一個可注入的、全應用級別的
MessageService
,用於傳送要顯示的訊息。creating an injectable, app-wide
MessageService
for sending messages to be displayed把
MessageService
注入到HeroService
中。injecting
MessageService
into theHeroService
當
HeroService
成功獲取了英雄資料時顯示一條訊息。displaying a message when
HeroService
fetches heroes successfully
建立 MessagesComponent
Create MessagesComponent
使用 CLI 建立 MessagesComponent
。
Use the CLI to create the MessagesComponent
.
ng generate component messages
CLI 在 src/app/messages
中建立了元件檔案,並且把 MessagesComponent
宣告在了 AppModule
中。
The CLI creates the component files in the src/app/messages
folder and declares the MessagesComponent
in AppModule
.
修改 AppComponent
的範本來顯示所產生的 MessagesComponent
:
Modify the AppComponent
template to display the generated MessagesComponent
.
<h1>{{title}}</h1>
<app-heroes></app-heroes>
<app-messages></app-messages>
你可以在頁面的底部看到來自的 MessagesComponent
的預設內容。
You should see the default paragraph from MessagesComponent
at the bottom of the page.
建立 MessageService
Create the MessageService
使用 CLI 在 src/app
中建立 MessageService
。
Use the CLI to create the MessageService
in src/app
.
ng generate service message
開啟 MessageService
,並把它的內容改成這樣:
Open MessageService
and replace its contents with the following.
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()
方法用於清空快取。
The service exposes its cache of messages
and two methods: one to add()
a message to the cache and another to clear()
the cache.
把它注入到 HeroService
中
Inject it into the HeroService
在 HeroService
中匯入 MessageService
。
In HeroService
, import the MessageService
.
import { MessageService } from './message.service';
修改這個建構函式,新增一個私有的 messageService
屬性引數。 Angular 將會在建立 HeroService
時把 MessageService
的單例注入到這個屬性中。
Modify the constructor with a parameter that declares a private messageService
property. Angular will inject the singleton MessageService
into that property when it creates the HeroService
.
constructor(private messageService: MessageService) { }
這是一個典型的“服務中的服務”場景: 你把 MessageService
注入到了 HeroService
中,而 HeroService
又被注入到了 HeroesComponent
中。
This is a typical "service-in-service" scenario: you inject the MessageService
into the HeroService
which is injected into the HeroesComponent
.
從 HeroService
中傳送一條訊息
Send a message from HeroService
修改 getHeroes()
方法,在獲取到英雄陣列時傳送一條訊息。
Modify the getHeroes()
method to send a message when the heroes are fetched.
getHeroes(): Observable<Hero[]> {
// TODO: send the message _after_ fetching the heroes
this.messageService.add('HeroService: fetched heroes');
return of(HEROES);
}
從 HeroService
中顯示訊息
Display the message from HeroService
MessagesComponent
可以顯示所有訊息, 包括當 HeroService
獲取到英雄資料時傳送的那條。
The MessagesComponent
should display all messages, including the message sent by the HeroService
when it fetches heroes.
開啟 MessagesComponent
,並且匯入 MessageService
。
Open MessagesComponent
and import the MessageService
.
import { MessageService } from '../message.service';
修改建構函式,新增一個 public 的 messageService
屬性。 Angular 將會在建立 MessagesComponent
的實例時 把 MessageService
的實例注入到這個屬性中。
Modify the constructor with a parameter that declares a public messageService
property. Angular will inject the singleton MessageService
into that property when it creates the MessagesComponent
.
constructor(public messageService: MessageService) {}
這個 messageService
屬性必須是公共屬性,因為你將會在範本中繫結到它。
The messageService
property must be public because you're going to bind to it in the template.
Angular 只會繫結到元件的公共屬性。
Angular only binds to public component properties.
繫結到 MessageService
Bind to the MessageService
把 CLI 產生的 MessagesComponent
的範本改成這樣:
Replace the CLI-generated MessagesComponent
template with the following.
<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
屬性上。
This template binds directly to the component's messageService
.
*ngIf
只有在有訊息時才會顯示訊息區。The
*ngIf
only displays the messages area if there are messages to show.*ngFor
用來在一系列<div>
元素中展示訊息列表。An
*ngFor
presents the list of messages in repeated<div>
elements.Angular 的事件繫結把按鈕的
click
事件繫結到了MessageService.clear()
。An Angular event binding binds the button's click event to
MessageService.clear()
.
當你把 最終程式碼 某一頁的內容新增到 messages.component.css
中時,這些訊息會變得好看一些。
The messages will look better when you add the private CSS styles to messages.component.css
as listed in one of the "final code review" tabs below.
為 hero 服務新增額外的訊息
Add additional messages to hero service
下面的例子展示了當用戶點選某個英雄時,如何傳送和顯示一條訊息,以及如何顯示該使用者的選取歷史。當你學到後面的路由一章時,這會很有幫助。
The following example shows how to send and display a message each time the user clicks on a hero, showing a history of the user's selections. This will be helpful when you get to the next section on Routing.
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);
}
}
重新整理瀏覽器,頁面顯示出了英雄列表。 滾動到底部,就會在訊息區看到來自 HeroService
的訊息。 點選“清空”按鈕,訊息區不見了。
Refresh the browser to see the list of heroes, and scroll to the bottom to see the messages from the HeroService. Each time you click a hero, a new message appears to record the selection. Use the "clear" button to clear the message history.
檢視最終程式碼
Final code review
本頁討論的程式碼檔案如下。
Here are the code files discussed on this page.
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);
}
}
小結
Summary
你把資料訪問邏輯重構到了
HeroService
類別中。You refactored data access to the
HeroService
class.你在根注入器中把
HeroService
註冊為該服務的提供者,以便在別處可以注入它。You registered the
HeroService
as the provider of its service at the root level so that it can be injected anywhere in the app.你使用 Angular 依賴注入機制把它注入到了元件中。
You used Angular Dependency Injection to inject it into a component.
你給
HeroService
中獲取資料的方法提供了一個非同步的函式簽名。You gave the
HeroService
get data method an asynchronous signature.你發現了
Observable
以及 RxJS 函式庫。You discovered
Observable
and the RxJS Observable library.你使用 RxJS 的
of()
方法返回了一個模擬英雄資料的可觀察物件 (Observable<Hero[]>
)。You used RxJS
of()
to return an observable of mock heroes (Observable<Hero[]>
).在元件的
ngOnInit
生命週期鉤子中呼叫HeroService
方法,而不是建構函式中。The component's
ngOnInit
lifecycle hook calls theHeroService
method, not the constructor.你建立了一個
MessageService
,以便在類別之間實現鬆耦合通訊。You created a
MessageService
for loosely-coupled communication between classes.HeroService
連同注入到它的服務MessageService
一起,注入到了元件中。The
HeroService
injected into a component is created with another injected service,MessageService
.