前回の投稿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
に置換することで挙動を確認できます。
本章は以上です。次章ではルーティングを説明します。