1. 程式人生 > >Angular 服務

Angular 服務

英雄指南的 HeroesComponent 目前獲取和顯示的都是模擬資料。

本節課的重構完成之後,HeroesComponent 變得更精簡,並且聚焦於為它的檢視提供支援。這也讓它更容易使用模擬服務進行單元測試。

如果你希望從 GitHub 上檢視我們提供測試的原始碼,你可以訪問下面的連結:https://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services

為什麼需要服務

元件不應該直接獲取或儲存資料,它們不應該瞭解是否在展示假資料。 它們應該聚焦於展示資料,而把資料訪問的職責委託給某個服務。

本節課,你將建立一個 HeroService,應用中的所有類都可以使用它來獲取英雄列表。 不要使用 new 來建立此服務,而要依靠 Angular 的依賴注入機制把它注入到 HeroesComponent 的建構函式中。

服務是在多個“互相不知道”的類之間共享資訊的好辦法。 你將建立一個 MessageService,並且把它注入到兩個地方:

  1. HeroService 中,它會使用該服務傳送訊息。
  2. MessagesComponent 中,它會顯示其中的訊息。

建立 HeroService

使用 Angular CLI 建立一個名叫 hero 的服務。

ng generate service hero

該命令會在src/app/hero.service.ts中生成HeroService類的骨架。HeroService類的程式碼如下:

src/app/hero.service.ts (new service)

import { Injectable } from '@angular/core';

 

@Injectable({

  providedIn: 

'root',

})

export class HeroService {

 

  constructor() { }

 

}

@Injectable() 服務

注意,這個新的服務匯入了 Angular 的 Injectable 符號,並且給這個服務類添加了 @Injectable() 裝飾器。 它把這個類標記為依賴注入系統的參與者之一。HeroService 類將會提供一個可注入的服務,並且它還可以擁有自己的待注入的依賴。 目前它還沒有依賴,但是很快就會有了

@Injectable() 裝飾器會接受該服務的元資料物件,就像 @Component() 對元件類的作用一樣。

獲取英雄資料

HeroService 可以從任何地方獲取資料:Web 服務、本地儲存(LocalStorage)或一個模擬的資料來源。

從元件中移除資料訪問邏輯,意味著將來任何時候你都可以改變目前的實現方式,而不用改動任何元件。 這些元件不需要了解該服務的內部實現。

這節課中的實現仍然會提供模擬的英雄列表

匯入 Hero 和 HEROES

import { Hero } from './hero';

import { HEROES } from './mock-heroes';

新增一個 getHeroes 方法,讓它返回模擬的英雄列表

getHeroes(): Hero[] {

  return HEROES;

}

提供(provide) HeroService

在要求 Angular 把 HeroService 注入到 HeroesComponent 之前,你必須先把這個服務提供給依賴注入系統稍後你就要這麼做。 你可以通過註冊提供商來做到這一點。提供商用來建立和交付服務,在這個例子中,它會對 HeroService 類進行例項化,以提供該服務。

現在,你需要確保 HeroService 已經作為該服務的提供商進行過註冊。 你要用一個注入器註冊它。注入器就是一個物件,負責在需要時選取和注入該提供商。

預設情況下,Angular CLI 命令 ng generate service 會通過給 @Injectable 裝飾器新增元資料的形式,用根注入器將你的服務註冊成為提供商。

如果你看看 HeroService 緊前面的 @Injectable() 語句定義,就會發現 providedIn 元資料的值是 'root':

@Injectable({

  providedIn: 'root',

})

@

Injectable

({
  providedIn: 'root',
})

當你在頂層提供該服務時,Angular 就會為 HeroService 建立一個單一的、共享的例項,並把它注入到任何想要它的類上。 在 @Injectable 元資料中註冊該提供商,還能允許 Angular 通過移除那些完全沒有用過的服務來進行優化。

要了解關於提供商的更多知識,參見提供商部分。 要了解關於注入器的更多知識,參見依賴注入指南

現在 HeroService 已經準備好插入到 HeroesComponent 中了。

這是一個過渡性的程式碼範例,它將會允許你提供並使用 HeroService。此刻的程式碼和最終程式碼相差很大。

修改 HeroesComponent

開啟 HeroesComponent 類檔案。

刪除 HEROES 的匯入語句,因為你以後不會再用它了。 轉而匯入 HeroService

src/app/heroes/heroes.component.ts (import HeroService)

import { HeroService } from '../hero.service';

把 heroes 屬性的定義改為一句簡單的宣告。

heroes: Hero[];

注入 HeroService

往建構函式中新增一個私有的 heroService,其型別為 HeroService

constructor(private heroService: HeroService) { }

這個引數同時做了兩件事:1. 聲明瞭一個私有 heroService 屬性,2. 把它標記為一個 HeroService 的注入點。

當 Angular 建立 HeroesComponent 時,依賴注入系統就會把這個 heroService 引數設定為 HeroService 的單例物件。

新增 getHeroes()

建立一個函式,以從服務中獲取這些英雄資料。

getHeroes(): void {

  this.heroes = this.heroService.getHeroes();

}

在 ngOnInit 中呼叫它

你固然可以在建構函式中呼叫 getHeroes(),但那不是最佳實踐。

讓建構函式保持簡單,只做初始化操作,比如把建構函式的引數賦值給屬性。 建構函式不應該做任何事。 它當然不應該呼叫某個函式來向遠端服務(比如真實的資料服務)發起 HTTP 請求。

而是選擇在 ngOnInit 生命週期鉤子中呼叫 getHeroes(),之後交由 Angular 處理,它會在構造出 HeroesComponent 的例項之後的某個合適的時機呼叫 ngOnInit。

ngOnInit() {

  this.getHeroes();

}

檢視執行效果

重新整理瀏覽器,該應用仍執行的一如既往。 顯示英雄列表,並且當你點選某個英雄的名字時顯示出英雄詳情檢視。

可觀察(Observable)的資料

HeroService.getHeroes() 的函式簽名是同步的,它所隱含的假設是 HeroService 總是能同步獲取英雄列表資料。 而 HeroesComponent 也同樣假設能同步取到 getHeroes() 的結果。

this.heroes = this.heroService.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)

import { Observable, of } from 'rxjs';

把 getHeroes 方法改成這樣:

getHeroes(): Observable<Hero[]> {

  return of(HEROES);

}

of(HEROES)會返回一個Observable<Hero[]>,它會發出單個值,這個值就是這些模擬英雄的陣列。

在 HTTP 教程中,你將會呼叫 HttpClient.get<Hero[]>() 它也同樣返回一個 Observable<Hero[]>,它也會發出單個值,這個值就是來自 HTTP 響應體中的英雄陣列。

在 HeroesComponent 中訂閱

HeroService.getHeroes 方法之前返回一個 Hero[], 現在它返回的是 Observable<Hero[]>

你必須在 HeroesComponent 中也向本服務中的這種形式看齊。

找到 getHeroes 方法,並且把它替換為如下程式碼(和前一個版本對比顯示):

heroes.component.ts (Observable)

getHeroes(): void {

  this.heroService.getHeroes()

      .subscribe(heroes => this.heroes = heroes);

}

heroes.component.ts (Original)

getHeroes(): void {

  this.heroes = this.heroService.getHeroes();

}

Observable.subscribe() 是關鍵的差異點。

上一個版本把英雄的陣列賦值給了該元件的 heroes 屬性。 這種賦值是同步的,這裡包含的假設是伺服器能立即返回英雄陣列或者瀏覽器能在等待伺服器響應時凍結介面。

當 HeroService 真的向遠端伺服器發起請求時,這種方式就行不通了。

新的版本等待 Observable 發出這個英雄陣列,這可能立即發生,也可能會在幾分鐘之後。 然後,subscribe 函式把這個英雄陣列傳給這個回撥函式,該函式把英雄陣列賦值給元件的 heroes屬性。

使用這種非同步方式,當 HeroService 從遠端伺服器獲取英雄資料時,就可以工作了

顯示訊息

在這一節,你將

  • 新增一個 MessagesComponent,它在螢幕的底部顯示應用中的訊息。
  • 建立一個可注入的、全應用級別的 MessageService,用於傳送要顯示的訊息。
  • 把 MessageService 注入到 HeroService 中。
  • 當 HeroService 成功獲取了英雄資料時顯示一條訊息。

建立 MessagesComponent

使用 CLI 建立 MessagesComponent

ng generate component messages

CLI 在 src/app/messages 中建立了元件檔案,並且把 MessagesComponent 宣告在了 AppModule 中。

修改 AppComponent 的模板來顯示所生成的 MessagesComponent

/src/app/app.component.html

<h1>{{title}}</h1>

<app-heroes></app-heroes>

<app-messages></app-messages>

你可以在頁面的底部看到來自的 MessagesComponent 的預設內容。

建立 MessageService

使用 CLI 在 src/app 中建立 MessageService

ng generate service message

開啟MessageService,並把它的內容改成這樣:

/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 中

重新開啟 HeroService,並且匯入 MessageService

/src/app/hero.service.ts (import MessageService)

import { MessageService } from './message.service';

修改這個建構函式,新增一個私有的 messageService 屬性引數。 Angular 將會在建立 HeroService 時把 MessageService 的單例注入到這個屬性中。

constructor(private messageService: MessageService) { }

這是一個典型的“服務中的服務”場景: 你把 MessageService 注入到了 HeroService 中,而 HeroService 又被注入到了 HeroesComponent 中。

從 HeroService 中傳送一條訊息

修改 getHeroes 方法,在獲取到英雄陣列時傳送一條訊息。

getHeroes(): Observable<Hero[]> {

  // TODO: send the message _after_ fetching the heroes

  this.messageService.add('HeroService: fetched heroes');

  return of(HEROES);

}

從 HeroService 中顯示訊息

MessagesComponent 可以顯示所有訊息, 包括當 HeroService 獲取到英雄資料時傳送的那條。

開啟 MessagesComponent,並且匯入 MessageService

/src/app/messages/messages.component.ts (import MessageService)

import { MessageService } from '../message.service';

修改建構函式,新增一個 public 的 messageService 屬性。 Angular 將會在建立 MessagesComponent 的例項時 把 MessageService 的例項注入到這個屬性中。

constructor(public messageService: MessageService) {}

這個messageService屬性必須是公共屬性,因為你將會在模板中繫結到它。

Angular 只會繫結到元件的公共屬性。

繫結到 MessageService

把 CLI 生成的 MessagesComponent 的模板改成這樣:

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 只有在有訊息時才會顯示訊息區。
  • *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

對應的檔案列表和程式碼連結如下:

檔名

原始碼

src/app/hero.service.ts https://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services/blob/master/src/app/hero.service.ts
src/app/heroes/heroes.component.ts https://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services/blob/master/src/app/heroes/heroes.component.ts
src/app/messages/messages.component.ts https://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services/blob/master/src/app/messages/messages.component.ts
src/app/messages/messages.component.html https://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services/blob/master/src/app/messages/messages.component.html
src/app/messages/messages.component.css https://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services/blob/master/src/app/messages/messages.component.css
src/app/app.module.ts https://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services/blob/master/src/app/app.module.ts
src/app/app.component.html https://github.com/cwiki-us-angular/cwiki-us-angular-tour-of-hero-services/blob/master/src/app/app.component.html

小結

  • 你把資料訪問邏輯重構到了 HeroService 類中。
  • 你在根注入器中把 HeroService 註冊為該服務的提供商,以便在別處可以注入它。
  • 你使用 Angular 依賴注入機制把它注入到了元件中。
  • 你給 HeroService 中獲取資料的方法提供了一個非同步的函式簽名。
  • 你發現了 Observable 以及 RxJS 庫。
  • 你使用 RxJS 的 of() 方法返回了一個模擬英雄資料的可觀察物件 (Observable<Hero[]>)。
  • 在元件的 ngOnInit 生命週期鉤子中呼叫 HeroService 方法,而不是建構函式中。
  • 你建立了一個 MessageService,以便在類之間實現鬆耦合通訊。
  • HeroService 連同注入到它的服務 MessageService 一起,注入到了元件中。

 

https://www.cwiki.us/display/AngularZH/Servic