Angular 服務
英雄指南的 HeroesComponent
目前獲取和顯示的都是模擬資料。
本節課的重構完成之後,HeroesComponent
變得更精簡,並且聚焦於為它的檢視提供支援。這也讓它更容易使用模擬服務進行單元測試。
如果你希望從 GitHub 上檢視我們提供測試的原始碼,你可以訪問下面的連結:https://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services
為什麼需要服務
元件不應該直接獲取或儲存資料,它們不應該瞭解是否在展示假資料。 它們應該聚焦於展示資料,而把資料訪問的職責委託給某個服務。
本節課,你將建立一個 HeroService
,應用中的所有類都可以使用它來獲取英雄列表。 不要使用 new
來建立此服務,而要依靠 Angular 的依賴注入機制把它注入到 HeroesComponent
的建構函式中。
服務是在多個“互相不知道”的類之間共享資訊的好辦法。 你將建立一個 MessageService
,並且把它注入到兩個地方:
HeroService
中,它會使用該服務傳送訊息。MessagesComponent
中,它會顯示其中的訊息。
建立 HeroService
使用 Angular CLI 建立一個名叫 hero
的服務。
|
該命令會在src/app/hero.service.ts
中生成HeroService
類的骨架。HeroService
類的程式碼如下:
src/app/hero.service.ts (new service)
'root' ,
|
@Injectable() 服務
注意,這個新的服務匯入了 Angular 的 Injectable 符號,並且給這個服務類添加了 @
Injectable()
裝飾器。 它把這個類標記為依賴注入系統的參與者之一。HeroService
類將會提供一個可注入的服務,並且它還可以擁有自己的待注入的依賴。 目前它還沒有依賴,但是很快就會有了。
@
Injectable()
裝飾器會接受該服務的元資料物件,就像 @
Component()
對元件類的作用一樣。
獲取英雄資料
HeroService
可以從任何地方獲取資料:Web 服務、本地儲存(LocalStorage)或一個模擬的資料來源。
從元件中移除資料訪問邏輯,意味著將來任何時候你都可以改變目前的實現方式,而不用改動任何元件。 這些元件不需要了解該服務的內部實現。
這節課中的實現仍然會提供模擬的英雄列表。
匯入 Hero
和 HEROES
。
|
新增一個 getHeroes
方法,讓它返回模擬的英雄列表。
|
提供(provide) HeroService
在要求 Angular 把 HeroService
注入到 HeroesComponent
之前,你必須先把這個服務提供給依賴注入系統。稍後你就要這麼做。 你可以通過註冊提供商來做到這一點。提供商用來建立和交付服務,在這個例子中,它會對 HeroService
類進行例項化,以提供該服務。
現在,你需要確保 HeroService
已經作為該服務的提供商進行過註冊。 你要用一個注入器註冊它。注入器就是一個物件,負責在需要時選取和注入該提供商。
預設情況下,Angular CLI 命令 ng generate service
會通過給 @
Injectable 裝飾器新增元資料的形式,用根注入器將你的服務註冊成為提供商。
如果你看看 HeroService
緊前面的 @
Injectable()
語句定義,就會發現 providedIn 元資料的值是 'root':
|
@
({
providedIn: 'root',
})
當你在頂層提供該服務時,Angular 就會為 HeroService
建立一個單一的、共享的例項,並把它注入到任何想要它的類上。 在 @
Injectable 元資料中註冊該提供商,還能允許 Angular 通過移除那些完全沒有用過的服務來進行優化。
要了解關於提供商的更多知識,參見提供商部分。 要了解關於注入器的更多知識,參見依賴注入指南。
現在 HeroService
已經準備好插入到 HeroesComponent
中了。
這是一個過渡性的程式碼範例,它將會允許你提供並使用 HeroService
。此刻的程式碼和最終程式碼相差很大。
修改 HeroesComponent
開啟 HeroesComponent
類檔案。
刪除 HEROES
的匯入語句,因為你以後不會再用它了。 轉而匯入 HeroService
。
src/app/heroes/heroes.component.ts (import HeroService)
|
把 heroes
屬性的定義改為一句簡單的宣告。
|
注入 HeroService
往建構函式中新增一個私有的 heroService
,其型別為 HeroService
。
|
這個引數同時做了兩件事:1. 聲明瞭一個私有 heroService
屬性,2. 把它標記為一個 HeroService
的注入點。
當 Angular 建立 HeroesComponent
時,依賴注入系統就會把這個 heroService
引數設定為 HeroService
的單例物件。
新增 getHeroes()
建立一個函式,以從服務中獲取這些英雄資料。
|
在 ngOnInit
中呼叫它
你固然可以在建構函式中呼叫 getHeroes()
,但那不是最佳實踐。
讓建構函式保持簡單,只做初始化操作,比如把建構函式的引數賦值給屬性。 建構函式不應該做任何事。 它當然不應該呼叫某個函式來向遠端服務(比如真實的資料服務)發起 HTTP 請求。
而是選擇在 ngOnInit 生命週期鉤子中呼叫 getHeroes(),之後交由 Angular 處理,它會在構造出 HeroesComponent 的例項之後的某個合適的時機呼叫 ngOnInit。
|
檢視執行效果
重新整理瀏覽器,該應用仍執行的一如既往。 顯示英雄列表,並且當你點選某個英雄的名字時顯示出英雄詳情檢視。
可觀察(Observable)的資料
HeroService.getHeroes()
的函式簽名是同步的,它所隱含的假設是 HeroService
總是能同步獲取英雄列表資料。 而 HeroesComponent
也同樣假設能同步取到 getHeroes()
的結果。
|
這在真實的應用中幾乎是不可能的。 現在能這麼做,只是因為目前該服務返回的是模擬資料。 不過很快,該應用就要從遠端伺服器獲取英雄資料了,而那天生就是非同步操作。
HeroService
必須等伺服器給出響應, 而 getHeroes()
不能立即返回英雄資料, 瀏覽器也不會在該服務等待期間停止響應。
HeroService.getHeroes()
必須具有某種形式的非同步函式簽名。
它可以使用回撥函式,可以返回 Promise
(承諾),也可以返回 Observable
(可觀察物件)。
這節課,HeroService.getHeroes()
將會返回 Observable
,因為它最終會使用 Angular 的 HttpClient.get
方法來獲取英雄資料,而 HttpClient.get()
會返回 Observable
。
可觀察物件版本的 HeroService
Observable
是 RxJS 庫中的一個關鍵類。
在稍後的 HTTP 教程中,你就會知道 Angular HttpClient 的方法會返回 RxJS 的 Observable
。 這節課,你將使用 RxJS 的 of()
函式來模擬從伺服器返回資料。
開啟 HeroService
檔案,並從 RxJS 中匯入 Observable
和 of
符號。
src/app/hero.service.ts (Observable imports)
|
把 getHeroes
方法改成這樣:
|
of(HEROES)
會返回一個Observable<Hero[]>
,它會發出單個值,這個值就是這些模擬英雄的陣列。
在 HTTP 教程中,你將會呼叫 HttpClient.get<Hero[]>()
它也同樣返回一個 Observable<Hero[]>
,它也會發出單個值,這個值就是來自 HTTP 響應體中的英雄陣列。
在 HeroesComponent
中訂閱
HeroService.getHeroes
方法之前返回一個 Hero[]
, 現在它返回的是 Observable<Hero[]>
。
你必須在 HeroesComponent
中也向本服務中的這種形式看齊。
找到 getHeroes
方法,並且把它替換為如下程式碼(和前一個版本對比顯示):
heroes.component.ts (Observable)
|
heroes.component.ts (Original)
|
Observable.subscribe()
是關鍵的差異點。
上一個版本把英雄的陣列賦值給了該元件的 heroes
屬性。 這種賦值是同步的,這裡包含的假設是伺服器能立即返回英雄陣列或者瀏覽器能在等待伺服器響應時凍結介面。
當 HeroService
真的向遠端伺服器發起請求時,這種方式就行不通了。
新的版本等待 Observable
發出這個英雄陣列,這可能立即發生,也可能會在幾分鐘之後。 然後,subscribe
函式把這個英雄陣列傳給這個回撥函式,該函式把英雄陣列賦值給元件的 heroes
屬性。
使用這種非同步方式,當 HeroService
從遠端伺服器獲取英雄資料時,就可以工作了。
顯示訊息
在這一節,你將
- 新增一個
MessagesComponent
,它在螢幕的底部顯示應用中的訊息。 - 建立一個可注入的、全應用級別的
MessageService
,用於傳送要顯示的訊息。 - 把
MessageService
注入到HeroService
中。 - 當
HeroService
成功獲取了英雄資料時顯示一條訊息。
建立 MessagesComponent
使用 CLI 建立 MessagesComponent
。
|
CLI 在 src/app/
messages 中建立了元件檔案,並且把 MessagesComponent
宣告在了 AppModule
中。
修改 AppComponent
的模板來顯示所生成的 MessagesComponent
:
/src/app/app.component.html
|
你可以在頁面的底部看到來自的 MessagesComponent
的預設內容。
建立 MessageService
使用 CLI 在 src/app
中建立 MessageService
。
|
開啟MessageService
,並把它的內容改成這樣:
/src/app/message.service.ts
|
該服務對外暴露了它的 messages 快取,以及兩個方法:add()
方法往快取中新增一條訊息,clear()
方法用於清空快取。
把它注入到 HeroService
中
重新開啟 HeroService
,並且匯入 MessageService
。
/src/app/hero.service.ts (import MessageService)
|
修改這個建構函式,新增一個私有的 messageService
屬性引數。 Angular 將會在建立 HeroService
時把 MessageService
的單例注入到這個屬性中。
|
這是一個典型的“服務中的服務”場景: 你把 MessageService
注入到了 HeroService
中,而 HeroService
又被注入到了 HeroesComponent
中。
從 HeroService
中傳送一條訊息
修改 getHeroes
方法,在獲取到英雄陣列時傳送一條訊息。
|
從 HeroService
中顯示訊息
MessagesComponent
可以顯示所有訊息, 包括當 HeroService
獲取到英雄資料時傳送的那條。
開啟 MessagesComponent
,並且匯入 MessageService
。
/src/app/messages/messages.component.ts (import MessageService)
|
修改建構函式,新增一個 public 的 messageService
屬性。 Angular 將會在建立 MessagesComponent
的例項時 把 MessageService
的例項注入到這個屬性中。
|
這個messageService
屬性必須是公共屬性,因為你將會在模板中繫結到它。
Angular 只會繫結到元件的公共屬性。
繫結到 MessageService
把 CLI 生成的 MessagesComponent
的模板改成這樣:
src/app/messages/messages.component.html
|
這個模板直接繫結到了元件的 messageService
屬性上。
*
ngIf 只有在有訊息時才會顯示訊息區。*
ngFor 用來在一系列<div>
元素中展示訊息列表。- Angular 的事件繫結把按鈕的
click
事件繫結到了MessageService.clear()
。
當你把 最終程式碼 某一頁的內容新增到 messages.component.css
中時,這些訊息會變得好看一些。
重新整理瀏覽器,頁面顯示出了英雄列表。 滾動到底部,就會在訊息區看到來自 HeroService
的訊息。 點選“清空”按鈕,訊息區不見了。
檢視最終程式碼
你的應用應該變成了這樣 線上例子 / 下載範例。本頁所提及的程式碼檔案如下。
如果你想直接在 stackblitz 執行本頁中的例子,請單擊連結:https://stackblitz.com/github/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services
本頁中所提及的程式碼如下:https://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services
對應的檔案列表和程式碼連結如下:
小結
- 你把資料訪問邏輯重構到了
HeroService
類中。 - 你在根注入器中把
HeroService
註冊為該服務的提供商,以便在別處可以注入它。 - 你使用 Angular 依賴注入機制把它注入到了元件中。
- 你給
HeroService
中獲取資料的方法提供了一個非同步的函式簽名。 - 你發現了
Observable
以及 RxJS 庫。 - 你使用 RxJS 的
of()
方法返回了一個模擬英雄資料的可觀察物件 (Observable<Hero[]>
)。 - 在元件的
ngOnInit
生命週期鉤子中呼叫HeroService
方法,而不是建構函式中。 - 你建立了一個
MessageService
,以便在類之間實現鬆耦合通訊。 HeroService
連同注入到它的服務MessageService
一起,注入到了元件中。