英雄指南——服務

版本: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)或一個模擬的數據源。目前,導入HeromockHeroes,并且在一個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 就會在適當的時機調用它。

更多關于生命周期鉤子的內容請看生命周期鉤子章節。

添加 OnInitAppComponent實現的接口列表,并編寫一個內部帶有初始化邏輯的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 語言教程異步編程: FuturesAsync 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() ,并觀察本應用是如何表現的。

下一步

路由

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 版本:Angular 5.0.0-alpha 依賴注入是重要的應用設計模式。它使用得非常廣泛,以至于幾乎每個人都稱...
    soojade閱讀 3,013評論 0 3
  • 版本:Angular 5.0.0-alpha AngularDart(本文檔中我們通常簡稱 Angular ) 是...
    soojade閱讀 851評論 0 4
  • 版本:4.0.0+2 有一些英雄指南應用的新需求: 添加一個儀表盤 視圖。 添加在英雄 視圖和 儀表盤 視圖之間導...
    soojade閱讀 1,338評論 0 0
  • Angular 2架構總覽 - 簡書http://www.lxweimin.com/p/aeb11061b82c A...
    葡萄喃喃囈語閱讀 1,504評論 2 13
  • 早幾天和同學逛了一下書城,我到的時候他們兩個已經逛了一會的,還沒找到要買的書。后面半小時之后不耐煩我就說:“你們來...
    我要做一個廢柴閱讀 1,761評論 9 13