AngularJS

0からのAngular⑤

ヒーローのデータをAppComponentが保持(データの定義も)していることは、よくない。
他のコンポーネントが簡単に利用することができず、拡張性が低いからだろう。
それを解決するためにServiceをつくる。
サービスがヒーローのデータの取得を行い、必要なコンポーネントに共有するということをする。

サービス用のファイルを作成する。

まずは、./src/app/hero.service.tsを作成する。
作成したら、以下の記述を行う。

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

@Injectable()
export class HeroService {

}

Injectableをインポートし、@Injectable()デコレータを使用している。
@Injectable()デコレータはTypeScriptにサービスに関するメタデータを出力するように指示する。メタデータはAngularが他の依存関係をサービスに入れる必要があることを指定するもの。
現時点では、何の依存関係もないがコードの一貫性を保つために初めに定義するようだ。

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

メソッドを加える。

@Injectable()
export class HeroService {
  getHeroes(): void {}
}

ヒーローのデータを移す

今回はデータベースなどからヒーローのデータを取得するなどはしない(できない)ので、ローカルにデータを用意する。
これまでapp.component.tsに定義していたHEROES定数を使用する。
サービスはこのデータを取得して、必要とするコンポーネントに渡すというのが役割なので、ヒーローのデータを保持する./src/app/mock-heroes.tsファイルを作成し、こちらに移す。
HEROESがサービスなどからimportできるように、exportするのをお忘れなく。

import { Hero } from './hero';

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

app.component.tsでは、HEROESがなくなったことで、heroesに変更を加える。
Hero型の要素が格納された配列と型の定義をしておく。

heroes: Hero[];

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

サービスでは、本来の目的であるヒーローのデータを取得するようにする。

import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';

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

HeroとHEROESをimportして、HEROESを返すメソッドを実装してある。

ヒーローのデータを使用する

まず、app.component.tsでHeroServiceをimportする。

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

ここでnewを使ってHeroServiceのインスタンスを作ってはいけない。理由は以下。

  • コンポーネントがインスタンスの作成方法を知る必要があり、サービスのコンストラクタの変更に併せて、修正する必要がでてしまう。
  • 都度newを使ってインスタンスを作り、それぞれがキャッシュした値を他のコンポーネントなどに共有させないため
  • コンポーネントがサービスの実装を固定してしあい、別の用途では、異なる実装をしなくてはならない。

newの代わりに以下のことを行う。

  • private変数としてコンストラクタに定義する
  • コンポーネントのprovidersメタデータを使用する

コンストラクタはこのようにする。

constructor(private heroService: HeroService) {  }

コンストラクタ自体はなにもしないが、privateでHeroService型のheroService変数の定義を自動で行う。
AngularはAppComponentの作成時に、HeroServiceのインスタンスを用意することがわかっている。

詳しくはここを読む。

インジェクターにHeroServiceの作成方法を教えるには、providers@Componentの下部に記述する。
providersはコンポーネントの作成時に、Angularに新しいインスタンスの作らせる。
そうすることで、コンポーネントとその子コンポーネントサービスを使用して、ヒーローのデータを取得できる。

HeroServiceのインスタンスの作成までできる用になったので、ヒーローのデータを取得したい。

AppComponentにメソッドを定義し、このメソッドを使用して取得する。

getHeroes(): void {
  this.heroes = this.heroService.getHeroes();
}

このメソッドで取得できる。
あとは、どこでこのメソッドを呼び出すか。
コンストラクタでも可能だが、それをやってはいけない。
理由は、コンストラクタは変数の初期化などを行うべきであり、データの取得など複雑なことを行ってはいけないとされているから。

そこでAngularのライフサイクルメソッドを使用する。
コンポーネントの作成時に呼ばれるngOnInitメソッドを使用する。
使用するためには、AngularからOnInitをimportし、Oninitを使用するを明示する。
ngOninitの中でgetHeroesを呼べばOK。

export class AppComponent implements OnInit  {
  constructor(private heroService: HeroService) {  }
  ngOnInit(): void {
    this.getHeroes();
  }
}

ここまで行うと、リストが表示されるようになる。
アウトプットは変わらないが、リファクタリングがされている。
Angular_QuickStart.png

処理を非同期化

実際データはサーバから取得することになる。
取得が終わるまでユーザーを待たせるのは良くないということで、取得を非同期で行うようにする。
promiseを使い、取得後にコールバック的に処理を行う。

HeroServiceを編集する。

getHeroes(): Promise<Hero[]> {
  return Promise.resolve(HEROES);
}

Promiseを使用した結果、AppComponentのheroesはPromiseがセットされることになった。
なので、処理の成功時のコールバックを渡すように記述を変更する。

getHeroes(): void {
  this.heroService.getHeroes().then(heroes => this.heroes = heroes);
}

処理の成功時にHeroServiceからHEROESを受取り、heroesに代入している。