填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫Home

服務

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.

要檢視本頁所講的範例程式,參閱現場演練 / 下載範例

For the sample application that this page describes, see the現場演練 / 下載範例.

為什麼需要服務

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.

  1. 注入到 HeroService 中,它會使用該服務傳送訊息

    Inject in HeroService, which uses the service to send a message.

  2. 注入到 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:

src/app/hero.service.ts (new service)
      
      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.

匯入 HeroHEROES

Import the Hero and HEROES.

src/app/hero.service.ts
      
      import { Hero } from './hero';
import { HEROES } from './mock-heroes';
    

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

Add a getHeroes method to return the mock heroes.

src/app/hero.service.ts
      
      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 application 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 application 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.

src/app/heroes/heroes.component.ts (import HeroService)
      
      import { HeroService } from '../hero.service';
    

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

Replace the definition of the heroes property with a declaration.

src/app/heroes/heroes.component.ts
      
      heroes: Hero[] = [];
    

注入 HeroService

Inject the HeroService

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

Add a private heroService parameter of type HeroService to the constructor.

src/app/heroes/heroes.component.ts
      
      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.

src/app/heroes/heroes.component.ts
      
      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 minimal 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.

src/app/heroes/heroes.component.ts
      
      ngOnInit() {
  this.getHeroes();
}
    

檢視執行效果

See it run

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

After the browser refreshes, the application 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.

src/app/heroes/heroes.component.ts
      
      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 application 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

ObservableRxJS 函式庫中的一個關鍵類別。

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 Observables. In this tutorial, you'll simulate getting data from the server with the RxJS of() function.

開啟 HeroService 檔案,並從 RxJS 中匯入 Observableof 符號。

Open the HeroService file and import the Observable and of symbols from RxJS.

src/app/hero.service.ts (Observable imports)
      
      import { Observable, of } from 'rxjs';
    

getHeroes() 方法改成這樣:

Replace the getHeroes() method with the following:

src/app/hero.service.ts
      
      getHeroes(): Observable<Hero[]> {
  const heroes = of(HEROES);
  return 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 application 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 the HeroService

  • 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.

src/app/app.component.html
      
      <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.

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() 方法用於清空快取。

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.

src/app/hero.service.ts (import 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.

src/app/hero.service.ts
      
      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.

src/app/hero.service.ts
      
      getHeroes(): Observable<Hero[]> {
  const heroes = of(HEROES);
  this.messageService.add('HeroService: fetched heroes');
  return 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.

src/app/messages/messages.component.ts (import MessageService)
      
      import { MessageService } from '../message.service';
    

修改建構函式,新增一個 publicmessageService 屬性。 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.

src/app/messages/messages.component.ts
      
      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.

src/app/messages/messages.component.html
      
      <div *ngIf="messageService.messages.length">

  <h2>Messages</h2>
  <button class="clear"
          (click)="messageService.clear()">Clear messages</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.

src/app/heroes/heroes.component.ts
      
      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 的訊息。 點選 Clear messages 按鈕,訊息區不見了。

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 messages 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[]> {
    const heroes = of(HEROES);
    this.messageService.add('HeroService: fetched heroes');
    return 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 the HeroService 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.