版本:4.0.0+2
隨著英雄指南應用的進化,你將會添加更多的需要訪問英雄數據的組件。
你將創建一個單獨的可復用的數據服務,并把它注入到需要它的組件中,而不是反復地復制和粘貼相同的代碼。使用一個單獨的服務,讓組件保持精簡,專注于為視圖提供支持,并且使用模擬服務使其易于編寫單元測試組件。
因為數據服務總是異步的,你將會使用一個基于 Future 版本的數據服務來完成本章。
當你按照本章完成之后,應用看起來應該是這樣的——在線示例 (查看源碼)。
我們離開的地方
在繼續英雄指南之前,驗證你是否有如下結構。如果沒有,回去查看前一章。
如果應用不運行了,啟動應用。當你做出修改時,通過刷新瀏覽器保持繼續運行。
創建英雄服務
客戶想要在不同的頁面上以不同的方式顯示英雄。現在用戶已經可以從列表中選擇一個英雄了。很快,你將添加一個儀表盤來顯示表現最好的英雄,并創建一個獨立視圖來編輯英雄的詳情。這三個視圖都需要英雄數據。
目前,AppComponent
顯示的是模擬數據。然而,定義英雄的數據不該是組件的任務,并且你不能很容易地在其它組件和視圖中共享這個英雄列表。在本章,你將移動獲取英雄數據的業務到一個單獨的服務中,它將提供數據,并在所有需要這個數據的組件之間共享此服務。
創建一個可注入的 HeroService
在lib/src
目錄下創建一個名為hero_service.dart
的文件。
服務文件的命名約定是——小寫的服務名后緊跟著
_service
。對于多個單詞的服務名,使用小寫蛇形。例如,SpecialSuperHeroService
服務的文件名應該是special_super_hero_service.dart
。
把這個類命名為HeroService
。
// lib/src/hero_service.dart (empty class)
import 'package:angular/angular.dart';
@Injectable()
class HeroService {
}
可注入的服務
注意你使用的 @Injectable() 注解。這告訴 Angular 編譯器HeroService
將會是一個注入的候補(更多信息很快就來)。
獲取英雄數據
HeroService
可以從任何地方獲取英雄數據:Web 服務、本地存儲(LocalStorage)或一個模擬的數據源。目前,導入Hero
和mockHeroes
,并且在一個getHeroes()
方法中返回模擬的英雄:
// lib/src/hero_service.dart
import 'package:angular/angular.dart';
import 'hero.dart';
import 'mock_heroes.dart';
@Injectable()
class HeroService {
List<Hero> getHeroes() => mockHeroes;
}
使用英雄服務
你已經準備好在其它組件中使用HeroService
了,先從AppComponent
開始吧。
導入HeroService
以便你可以在代碼中引用它。
// lib/app_component.dart (hero service import)
import 'src/hero_service.dart';
不要對 HeroService 使用 new
AppComponent
應該如何獲取HeroService
的實例呢?
你可以像這樣使用new
來創建一個新的HeroService
實例:
// lib/app_component.dart (excerpt)
HeroService heroService = new HeroService(); // 不要這樣做
然而,這并不是一個好的選擇,原因如下:
- 組件必須知道如何創建一個
HeroService
實例。如果你修改了HeroService
的構造函數,你就必須找到并更新創建過此服務的每一處地方。在多處地方修補代碼容易引起錯誤并增加測試的負擔。 - 每使用一次
new
你就創建一個服務。假如服務緩存英雄并和其它的服務或組件共享這個緩存呢?你做不到那樣。 -
AppComponent
受限于一個特定的HeroService
實現,難以針對不同的情景選擇不同的實現,例如,離線操作或為測試使用不同的模擬版本。
注入 HeroService
添加如下所述的行,而不是使用new
表達式:
- 添加一個私有的
HeroService
屬性。 - 添加一個構造函數初始化這個私有屬性。
- 添加
HeroService
到組件的providers
元數據。
下面就是屬性和構造函數:
// lib/app_component.dart (constructor)
final HeroService _heroService;
AppComponent(this._heroService);
構造函數除了設置_heroService
屬性什么也沒做。_heroService
的類型HeroService
標志著構造函數的參數為HeroService
的注入點。
現在,當創建一個新的AppComponent
組件時,Angular 知道提供一個HeroService
的實例。
更多關于依賴注入的內容,請看依賴注入章節。
注入器(injector)還不知道怎樣創建HeroService
。如果你現在運行代碼,Angular會失敗,并報錯:
EXCEPTION: No provider for HeroService! (AppComponent -> HeroService)
添加如下的providers
列表作為@Component
注解最后的參數,來告訴注入器如何制造HeroService
實例。
// lib/app_component.dart (providers)
providers: const [HeroService],
providers
參數告訴 Angular,當它創建一個AppComponent
組件時,創建一個新鮮的HeroService
的實例。AppComponent
及其子組件可以使用這個服務來獲取英雄數據。
AppComponent.getHeroes() 方法
添加一個getHeroes()
方法到 app component,并且移除heroes
初始化程序:
// lib/app_component.dart (heroes and getHeroes)
List<Hero> heroes;
void getHeroes() {
heroes = _heroService.getHeroes();
}
ngOnInit 生命周期鉤子
AppComponent
應該毫無問題地獲取和顯示英雄。
你可能忍不住在構造函數中調用getHeroes()
方法,但構造函數不應該包含復雜的邏輯,尤其是一個呼叫服務器的構造函數,比如一個數據存取方法。構造函數應該單純用來初始化,比如把構造函數的參數賦值給屬性。
你可以實現 Angular 的 ngOnInit 生命周期鉤子,來使 Angular 調用getHeroes()
方法。Angular 為組件生命周期的幾個關鍵時刻提供了接口:創建時、每次變化后,以及最終被銷毀時。
每個接口有一個單獨的方法。當組件實現了那個方法,Angular 就會在適當的時機調用它。
更多關于生命周期鉤子的內容請看生命周期鉤子章節。
添加 OnInit 到AppComponent
實現的接口列表,并編寫一個內部帶有初始化邏輯的ngOnInit
方法。Angular 會在適當的時候調用它。在這個例子中,通過調用getHeroes()
初始化。
class AppComponent implements OnInit {
void ngOnInit() => getHeroes();
}
刷新瀏覽器。應用應該顯示一列英雄,并且當用戶點擊英雄名時,顯示英雄詳情視圖。
異步的英雄服務
HeroService
立刻返回了一個模擬英雄的列表;它的getHeroes()
簽名是同步的:
// lib/src/hero_service.dart (getHeroes)
List<Hero> getHeroes() => mockHeroes;
最終,英雄數據會來自于一個遠程服務器。當使用一個遠程服務器時,用戶不得不等待服務器響應;此外,在等待時你也不能夠阻塞 UI。
要協調視圖和響應,可以使用 Futures,這是一種改變getHeroes()
方法簽名的異步技術。
英雄服務返回一個 Future
一個 Future 表示一個未來的計算或值。使用一個Future
,你可以注冊回調函數,它將會在計算完成(結果準備好)或計算出錯需要報告時被調用。
這里只是簡單的說明。更多關于 Futures 的信息請看 Dart 語言教程的 異步編程: Futures。
添加一個 dart:async 的導入,因為它定義了Future
,并且更新 HeroService
,使用Future
作為 getHeroes()
方法的返回值類型:
// lib/src/hero_service.dart (excerpt)
Future<List<Hero>> getHeroes() async => mockHeroes;
你仍然在使用模擬數據。通過返回一個模擬英雄立即可用的Future
,模擬了一個超快、零延遲的服務器行為。
設置返回值類型為
Future
,使一個方法自動變成了異步方法。關于異步函數的更多信息,請看 Dart 語言教程的聲明異步函數。
處理 Future
由于HeroService
改變的結果,app 組件的heroes
屬性現在是一個Future
而不是一個英雄的列表。你必須改變這種實現,在它完成時處理Future
結果。當Future
成功完成,你就會有英雄來顯示了。
這里是目前的實現:
// lib/app_component.dart (synchronous getHeroes)
void getHeroes() {
heroes = _heroService.getHeroes();
}
傳遞一個回調函數作為Future.then()
方法的參數:
// lib/app_component.dart (asynchronous getHeroes)
void getHeroes() {
_heroService.getHeroes().then((heroes) => this.heroes = heroes);
}
這個回調函數設置組件的heroes
屬性到通過服務返回的英雄列表。
刷新瀏覽器。應用仍然運行,顯示一列英雄,并使用一個詳情視圖響應名稱的選擇。
使用 async/await
一個異步方法包含一個或多個Future.then()
方法難以閱讀和理解。謝天謝地,Dart 的async/await
語言特性讓你寫異步代碼就像寫同步代碼一樣。重寫getHeroes()
:
// lib/app_component.dart (revised async/await getHeroes)
Future<Null> getHeroes() async {
heroes = await _heroService.getHeroes();
}
Future<Null>
返回值類型等價于異步的void
。
更多關于使用async/await
異步變成的知識,請看 Dart 語言教程異步編程: Futures的 Async and await部分。
在本章的結尾,附錄:慢一點 描述了連接不良的應用可能是什么樣的。
回顧應用結構
確認經過所有重構之后,應該有如下文件結構:
我們已經走過的路
以下就是你在本章的收獲:
- 創建了一個能被多個組件共享的服務類。
- 使用
ngOnInit
生命周期鉤子,在AppComponent
激活時獲取英雄數據。 - 定義
HeroService
作為AppComponent
的一個提供器。 - 把服務設計為返回一個 Future,并且組件從 Future 中獲取數據。
附錄:慢一點
要模擬一個緩慢的連接,添加如下的getHeroesSlowly()
方法到HeroService
。
// lib/src/hero_service.dart (getHeroesSlowly)
Future<List<Hero>> getHeroesSlowly() {
return new Future.delayed(const Duration(seconds: 2), getHeroes);
}
像 getHeroes
一樣,它也返回一個Future
,但這個 Future
會在完成之前等待兩秒鐘。
回到 AppComponent
,用 getHeroesSlowly()
替換掉 getHeroes()
,并觀察本應用是如何表現的。
下一步