前回の投稿Angluar2のクイックスタートとチュートリアルを実施 - その4の続きです。
前回作成したソースを使用します。
本投稿の参照元(英語)
(バージョンが異なりますが、大きな影響はないと思います。)
Services - ts
なお、本章記載中にAngular2の正式最終リリースがされちゃいました。
http://angularjs.blogspot.jp/2016/09/angular2-final.html
本章で学ぶこと
以下を学習します。
- 複数のコンポーネントで共有可能なサービスクラスを作成する。
-
ngOnInitライフサイクルフック -
AppComponentのプロバイダとしてHeroServiceを定義する - プロミスの作り方と使い方
実行結果
本章を完了すると以下リンク先のようなものが出来上がります。
見た目は前回,前々回のアプリと変わっていません。
live-examples
事前準備
以下のコマンドによりサーバの起動を行います。プログラムの変更が即座にブラウザに反映されます。(サーバを落としていなければ不要です。)
cd angular2-tour-of-heroes
npm start
ヒーローサービスの作成
app/hero.service.tsを作成します。
'@angular/core'からInjectableをインポートし、HeroServiceクラスを定義します。
import { Injectable } from '@angular/core';
@Injectable()
export class HeroService {
}
Injectableサービス
Injectableのインポートにより、@Injectable()デコレータが使用可能になります。
※()は必ず必要ですので忘れない様にしてください。
TypeScriptは@Injectable()を見、AngularがDIできるメタデータを発行します。
まだここではHeroServiceは依存対象を何も持っていません。
@Injectable()デコレータを使用することは一貫性と将来性のためのベストプラクティスです。
ヒーロー一覧の取得
getHeroesメソッドをスタブとして追加します。実装は後で行います。
@Injectable()
export class HeroService {
getHeroes(): void {} // stub
}
このアプリを使用するユーザは、何のサービスからデータが取得されているかを知りません。ウェブサービスからかもしれないし、ローカルストレージからかもしれないし、モックからかもしれません。
また、コンポーネントからデータアクセス部分を取り除くことは良い手法です。ヒーロー情報に触れるためにコンポーネントをいじる必要がないためです。
ヒーローのモックデータ
app/app.component.tsにあるヒーローのモックデータを別ファイル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'}
];
app.component.tsのHEROES配列を削除した代わりに、AppComponentクラスに、heroes: Hero[]を定義してあげます。
heroes: Hero[];
ヒーローのモックデータに戻る
HeroServiceで、モックのHEROESをインポートし、同クラスのgetHeroesメソッドでHEROESを返す様にします。サーバからデータを取得してくるイメージです。
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を使用する準備が整いました。まずはHeroServiceをインポートします。
import { HeroService } from './hero.service';
尚、HeroServiceは、以下の様にnewするべきではありません。
heroService = new HeroService(); // don't do this
その理由は以下のとおりです。
- コンポーネントは
HeroServiceの作られ方を知る必要がある。コンストラクタを修正しようとした場合に使用している全箇所で修正が必要になるためテストがめんどうになる - 何のサービスがキャッシュしているかわからず、また共有できない
-
AppComponentを特定のサービスHeroServiceにしてしまうと、変更に弱くなる
などなど
HeroServiceのインジェクト(注入)
HeroServiceを使用するため、インジェクトする側(AppComponent)に以下の2箇所を変更します。
- コンストラクタを追加する。(プライベートなプロパティが定義される)
-
Componentデコレータにprovidesを追加する
以下のようにAppComponentクラスにコンストラクタ1を追加します。
constructor(private heroService: HeroService) { }
コンストラクタ自体は何もしませんが、引数がprivateなheroServiceプロパティとして定義され、AppComponentがインジェクトするクラスの対象となります。(コンストラクタ内でthis.heroService = heroServiceが暗黙で定義されます)
これにより、AppComponentクラスが作成されたときに、HeroServiceのインスタンスが提供されます。
インジェクトする側は、HeroServiceの使い方を知らないため、現時点では以下のエラーで失敗します。
EXCEPTION: No provider for HeroService! (AppComponent -> HeroService)
インジェクトする側はHeroServiceをプロバイダー(サービス提供者)として登録
する必要があります。app/app.component.tsの@Componentデコレータにproviders配列と、その要素にHeroServiceを追加します。
@Component({
selector: 'my-app',
template: ~省略~,
styles: ~省略~,
providers: [HeroService]
})
このprovidersは、AppComponentがつくられる時にHeroServiceのインスタンスを生成する様に指示します。AppComponentはHeroServiceサービスを使用し、ヒーロー一覧を取得したり、サービスのチャイルドコンポーネントを使ったりできます。
- DI(Dependency Injection)については、@hshimoさんの猿でも分かる! Dependency Injection: 依存性の注入にあります。
- Angular2のDIについては、@laco0416さんのAngular2のDIを知るに詳しく書いてあります。
- Angular2公式のDIの説明はこちら(英語)
AppComponentのgetHeroesメソッド
heroServiceを使いましょう。AppComponentクラスにgetHeroes()メソッドを作成します。
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}
このgetHeroes()メソッドにより、サービスからデータを取得できますが、このメソッドは、どこで呼び出すべきでしょうか。
ngOnInitライフサイクルフック
getHeroes()メソッドはコンストラクタではなく、ngOnInitライフサイクルフック2にて呼び出します。経験上コンストラクタにはできるだけロジック(特にサーバアクセス)を書かない方がいいためです。
Angularでは重要な場面(作成時、変更時、デストラクト時)で実行する様々なインタフェース(ライフサイクルフック)を持っています。これらのインタフェースはシングルメソッドであり、実装することにより呼び出されます。
ライフサイクルフックについての詳細はこちら(英語)
ngOnInit以外のI/Fについても記載があります。
ngOnInitは以下の様に使用します。データがバインドされ、コンストラクタが実行された後、ngOnInit()は呼ばれます。
import { OnInit } from '@angular/core';
export class AppComponent implements OnInit {
ngOnInit(): void {
this.getHeroes();
}
}
これで期待通り動作しますが、もうちょっと修正します。
非同期サービスとプロミス
HeroServiceはモックデータの取得なので、即時に値を取得できますが、リモートサーバなどで即答を期待できない場合プロミスを使います。
プロミスについては@koki_cheeseさんの今更だけどPromise入門が勉強になります。
プロミスでHeroServiceを作成する
プロミスは、処理対象の準備ができたら呼び出し元へコールバックします。例えば非同期サーバーに問い合わせを行い、返事がもらえたらその結果を適切に処理します。
HeroServiceのgetHeroの戻り値をviodからPromiseへ、また、Promiseを返す様にします。3
getHeroes(): Promise<Hero[]> {
return Promise.resolve(HEROES);
}
プロミスの挙動
現在AppComponentクラスのgetHeroesメソッドは以下です。
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}
HeroServiceの変更により、getHeroes()メソッドからの戻り値Promise<Hero[]>を正しくthis.heroesプロパティの型Hero[]に変更するため、以下の様に変更します。
getHeroes(): void {
this.heroService.getHeroes().then(heroes => this.heroes = heroes);
}
ここの=>は、アロー関数を使用しています。Javascriptのアロー関数については、@you21979@githubさんのアロー関数の個人的なハマりどころまとめが参考になります。
ここでは、AppComponentクラスのプロパティheroes: Hero[]にHeroServiceクラスからの戻り値Promose<Hero[]>のHero[]を代入しています。
尚、TypeScriptではクラスメンバを表すthisは必須です。
これで、プロミスの実装ができました。
補足: 遅延
今のモックは即答しますが、サーバとの接続が遅い場合のシミュレーションを行います。
HeroServiceクラスにHeroクラスをインポートし、getHeroesSlowlyメソッドを作成します。
getHeroesSlowly(): Promise<Hero[]> {
return new Promise<Hero[]>(resolve =>
setTimeout(resolve, 2000)) // delay 2 seconds
.then(() => this.getHeroes());
}
getHeroes()の様に動作しますが、2秒間ディレイします。
AppComponentクラスのheroService.getHeroesをheroService.getHeroesSlowlyに置換することで挙動を確認できます。
本章は以上です。次章ではルーティングを説明します。