JavaScript
TypeScript
angular
Angular2
Angular4

[和訳] 5. Services (Angular公式チュートリアル)

More than 1 year has passed since last update.

この投稿は、Angular公式チュートリアルの 5. Services を和訳した記事です。
職場で行うAngular勉強会を円滑に進めることを目的としています。

筆者自身もAngularについては初級者で、英語の知識も乏しいため、記事内容への指摘がありましたら是非コメントをお願いします。

なお、この記事はAngular v4に対応したチュートリアルの和訳記事です。
最新版のチュートリアルではありませんのでご注意ください。

【チュートリアル一覧】
1. Introduction : 和訳ページ / 原文
2. The Hero Editor : 和訳ページ / 原文
3. Master/Detail : 和訳ページ / 原文
4. Multiple Components : 和訳ページ / 原文
5. Services : このページです / 原文
6. Routing : 和訳ページ作成中 / 原文
7. HTTP : 和訳ページ作成中 / 原文


Services

Tour of Heroesアプリが進化するにつれて、ヒーロのーデータにアクセスする必要のあるコンポーネントが更に追加されるでしょう。

同じコードを繰り返しコピーして貼り付ける代わりに、再利用可能な単一のデータサービスを作成し、それを必要とするコンポーネントに挿入します。
別のサービスを使用することで、コンポーネントの傾きをなくし、ビューをサポートすることに重点を置いて、モックサービスでコンポーネントを単体テストすることが容易になります。

データサービスは常に非同期であるため、Promiseベースのデータサービスでページを完成させます。

このページが完成したら、アプリはこの例のようになります。
https://v4.angular.io/generated/live-examples/toh-pt4/eplnkr.html

前回どこで終わったか

Tour of Heroesの続きに進む前に、以下の構造を持っていることを確認してください。 そうでない場合は、前のページに戻ります。
service_1.PNG

ヒーローサービスを作成する

HeroServiceを作成する

appフォルダにhero.service.tsという名前のファイルを作成します。

サービスファイルの命名規則は、小文字のサービス名の後に.serviceを付けたものです。
複数語のサービス名の場合は、ダッシュケースを小さくしてください。
たとえば、SpecialSuperHeroServiceのファイル名はspecial-super-hero.service.tsです。

HeroServiceクラスに名前を付け、他のファイルからimportできるようにexportします。

src/app/hero.service.ts(starting_point)
import { Injectable } from '@angular/core';

@Injectable()
export class HeroService {
}

注入可能なサービス

AngularのInjectable関数をインポートし、その関数を@Injectable()デコレータとして適用したことに注目してください。

かっこを忘れないでください。それらを省略すると、調査が困難なエラーが発生します。

@Injectable()デコレータは、サービスに関するメタデータを出力するようにTypeScriptに指示します。
メタデータは、Angularが他の依存関係をこのサービスに注入する必要があることを指定します。

HeroServiceは現時点では依存関係はありませんが、最初から@Injectable()デコレータを適用すると、一貫性と将来性が保証されます。

ヒーローデータを取得する

getHeroes()メソッドのスタブを追加してください。

src/app/hero.service.ts(getHeroes_stub)
@Injectable()
export class HeroService {
  getHeroes(): void {} // stub
}

HeroServiceは、Webサービス、ローカルストレージ、またはモックのデータソースのどこからでもHeroデータを取得できます。
コンポーネントからのデータアクセスを削除するということは、ヒーローデータを必要とするコンポーネントに触れることなく、いつでも実装に関する考えを変えることができるということを意味します。

モックのヒーローデータを移動する

app.component.tsからHEROES配列を切り取り、mock-heroes.tsという名前のappフォルダにある新しいファイルに貼り付けます。
さらに、ヒーロー配列がHeroクラスを使用するため、import {Hero} ...ステートメントをコピーします。

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

export const HEROES: Hero[] = [
  { id: 11, name: 'Mr. Nice' },
  { id: 12, name: 'Narco' },
  { id: 13, name: 'Bombasto' },
  { id: 14, name: 'Celeritas' },
  { id: 15, name: 'Magneta' },
  { id: 16, name: 'RubberMan' },
  { id: 17, name: 'Dynama' },
  { id: 18, name: 'Dr IQ' },
  { id: 19, name: 'Magma' },
  { id: 20, name: 'Tornado' }
];

HEROES定数は、HeroServiceなど他の場所でもimportできるようにexportされます。

app.component.tsでは、HEROES配列を切り取った箇所で、初期化されていないheroesプロパティを追加します。

src/app/app.component.ts(heroes_property)
heroes: Hero[];

モックのヒーローデータを返す

HeroServiceに戻り、モックのHEROESをインポートし、getHeroes()メソッドからそれを返します。
HeroServiceは次のようになります。

src/app/hero.service.ts
import { Injectable } from '@angular/core';

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

@Injectable()
export class HeroService {
  getHeroes(): Hero[] {
    return HEROES;
  }
}

ヒーローサービスをインポートする

HeroServiceを他のコンポーネントで使用する準備が整ったので、まずはAppComponentから始めていきます。
HeroServiceをインポートして、コード内で参照できるようにします。

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

HeroServiceではnewを使用しない

AppComponentは、実行時の具体的なHeroServiceインスタンスをどのように取得すればよいのでしょうか?

あなたは次のようにHeroServiceの新しいインスタンスを新規作成することができるでしょう。

src/app/app.component.ts
heroService = new HeroService(); // don't do this

ただし、この方法は理想的ではありません。理由は次のとおりです。

  • コンポーネントはHeroServiceを作成する方法を知っていなければなりません。HeroServiceコンストラクターを変更する場合は、サービスを作成したすべての場所を見つけて更新する必要があります。 複数の場所でコードを修正すると、エラーが発生しやすくなり、テストの負担が増します。
  • newを使用するたびにサービスを作成します。 サービスがヒーローのキャッシュを行い、そのキャッシュを他のコンポーネントと共有する場合、どうなるでしょうか? 実現できないでしょう。
  • AppComponentHeroServiceの特定の実装にロックされているため、オフラインでの操作やテスト用に異なるバージョンのモックの使用など、さまざまなシナリオの実装を切り替えるのは難しいでしょう。

HeroServiceを注入する

newを使用する代わりに、次の2行を追加します。

  • プライベートプロパティも定義するコンストラクタを追加する
  • コンポーネントのproviderのメタデータにHeroServiceを追加する

コンストラクタの追加:

src/app/app.component.ts(constructor)
constructor(private heroService: HeroService) { }

コンストラクタ自体は何もしません。
このパラメータは、プライベートのheroServiceプロパティを定義すると同時に、そのプロパティをHeroServiceの注入が行われる場所として識別します。

そしてAngularは、AppComponentを作成するときにHeroServiceのインスタンスを提供することを知っています。

依存性注入の詳細については、Dependency Injectionページを参照してください。

しかしInjectorは、HeroServiceの作成方法をまだ分かりません。
今すぐコードを実行した場合、Angularはこのエラーで失敗します。

EXCEPTION: No provider for HeroService! (AppComponent -> HeroService)

InjectorにHeroServiceの作成方法を教えるには、@Componentsで呼び出されたコンポーネントメタデータの一番下に次のproviders配列プロパティを追加します。

src/app/app.component.ts(providers)
providers: [HeroService]

providers配列は、AppComponentを作成するときにHeroServiceの新しいインスタンスを作成するようにAngularに指示します。
AppComponentとその子コンポーネントは、そのサービスを使用してヒーローデータを取得できます。

AppComponentgetHeroes()

サービスはプライベートのheroService変数にあります。
そのサービスを呼び出して、データを1行にまとめることができます。

src/app/app.component.ts
this.heroes = this.heroService.getHeroes();

あなたは1つの行をラップするために特別なメソッドを必要としません。 とにかくこれを書いてください。

src/app/app.component.ts(getHeroes)
getHeroes(): void {
  this.heroes = this.heroService.getHeroes();
}

ngOnInitライフサイクルフック

AppComponentは問題なくヒーローデータを取得して表示する必要があります。

コンストラクタでgetHeroes()メソッドを呼び出すことができますが、コンストラクタには複雑なロジック、特にデータアクセスメソッドなどのサーバーを呼び出すロジックが含まれてはいけません。
コンストラクタは、コンストラクタパラメータをプロパティへ紐づけるような単純な初期化のためのものです。

AngularにgetHeroes()を呼ばせるために、AngularのngOnInitライフサイクルフックを実装できます。
Angularは、コンポーネントライフサイクルに関する重要な瞬間(作成時、変更後、および最終的な破棄時)にアクセスするためのinterfaceを提供しています。

各interfaceには1つのメソッドがあります。
コンポーネントがそのメソッドを実装すると、しかるべきタイミングでAngularがそのメソッドを呼び出します。

ライフサイクルフックの詳細については、Lifecycle Hooksページを参照してください。

OnInitインターフェイスの基本的な概要は次のとおりです。(これをコードにコピーしないでください)

src/app/app.component.ts
import { OnInit } from '@angular/core';

export class AppComponent implements OnInit {
  ngOnInit(): void {
  }
}

OnInitインターフェイスの実装をエクスポートステートメントに追加します。

export class AppComponent implements OnInit {}

初期化ロジックを内部に持つngOnInitメソッドを記述します。
Angularは適切なタイミングでこのメソッドwp呼び出します。
この場合、getHeroes()を呼び出して初期化します。

src/app/app.component.ts(ng-on-init)
ngOnInit(): void {
  this.getHeroes();
}

ヒーロー名をクリックすると、ヒーローのリストとヒーローの詳細ビューが表示され、期待どおりに実行されるはずです。

非同期なサービスとPromise

HeroServiceはモックヒーローのリストを直ちに返します。そのgetHeroes()シグネチャは同期しています。

src/app/app.component.ts
this.heroes = this.heroService.getHeroes();

最終的に、主人公のデータはリモートサーバーから取得されます。
リモートサーバーを使用する場合、ユーザーはサーバーが応答するのを待つ必要はありません。
そのため、待機中にUIをブロックすることはできません。

ビューをレスポンスと調整するには、Promisesを使用します
これは、getHeroes()メソッドのシグネチャを変更する非同期技術です。

ヒーローサービスでPromiseを作成する

Promiseは、基本的には、結果の準備ができたらコールバックすることを約束します。
非同期サービスに何らかの作業を行い、コールバック関数を与えるように依頼します。
サービスはその作業を行い、最終的には結果またはエラーで関数を呼び出します。

このPromiseを返すgetHeroes()メソッドでHeroServiceを更新します。

src/app/hero.service.ts(抜粋)
getHeroes(): Promise<Hero[]> {
  return Promise.resolve(HEROES);
}

あなたはまだモックデータを使っています。
即座に解決されたPromiseをモックヒーローと結びつけて返すことで、超高速でゼロ遅延のサーバーの動作をシミュレートしています。

Promiseの振る舞い

HeroServiceへの変更の結果、this.heroesはヒーローの配列ではなくPromiseが設定されます。

src/app/app.component.ts(getHeroes(修正前))
getHeroes(): void {
  this.heroes = this.heroService.getHeroes();
}

Promiseが解決されたら、それを実行するように実装を変更する必要があります。
Promiseが正常に解決すると、あなたはヒーローを表示します。

Promiseのthen()メソッドへの引数としてコールバック関数を渡します。

src/app/app.component.ts(getHeroes(修正後))
getHeroes(): void {
  this.heroService.getHeroes().then(heroes => this.heroes = heroes);
}

コールバックは、コンポーネントのheroesプロパティをサービスが返すヒーローの配列に設定します。
アプリはまだ実行中で、ヒーローのリストを表示し、詳細ビューで名前選択に応答します。