はじめに
Angularチュートリアル:Tour of Heroesを読み進めていきます。
開発環境
- OS: Windows10
- Nodeバージョン: 18.18.0
- Angular CLI バージョン: 17.3.8
- エディタ: VSCode
4. サービスの追加
4. サービスの追加を読んでいきます。
サービスの必要性
チュートリアルでは、なぜサービスが必要なのかについて記載されています。
今まではコンポーネント内で値の取得を行ってきました。
しかし本来コンポーネントはデータの受け渡しに集中し、それ以外のロジック面(データの取得や保存、編集など)はそれを責務とする人に委託すべきです。
その「それを責務とする人」というのが、「サービス」です。
MVCのC(Controller)に該当するポジションという感じでしょうか。
関連コマンド
ng generate service %サービス名%
チュートリアルでは、ng generate service hero
コマンドを実行して、appフォルダ直下にhero.service.ts ファイルを生成しています。
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class HeroService {
constructor() { }
}
ソースコードを読み解いてみる
Injectable
@Injectableデコレーター
DI(Dependency Injection)を実現するための手法。
クラスの提供および注入可能にするために使用されます。
providedInプロパティ
サービスの適用範囲を指定できます。
Injectableにて、以下のような言及があります。
- 'root' : The application-level injector in most apps.
- 'platform' : A special singleton platform injector shared by all applications on the page.
- 'any' : Provides a unique instance in each lazy loaded module while all eagerly loaded modules share one instance. This option is DEPRECATED.
依存性の注入を理解するではrootを指定する例について言及しています。
ルートレベルでサービスを提供すると、AngularはHeroServiceの単一の共有インスタンスを作成し、それを要求するクラスに注入します。
ついでにCopilotに聞いてみると、以下のような回答を得られました。
@Injectable デコレーターには providedIn プロパティがあり、サービスの提供範囲を指定できます。一般的な値は以下の通りです:
- root: アプリケーション全体でシングルトンインスタンスを提供します。最も一般的な設定です。
- any: 各モジュールで新しいインスタンスを提供します。
- platform: プラットフォーム全体でシングルトンインスタンスを提供します。
とりあえずrootを指定するのが一般的のようです。
また、anyは非推奨とのことなので使わない方が良いでしょう。
複数のアプリケーションが乗っかるプラットフォーム全体でDIさせたいときはplatformを指定するようです。
チュートリアルでは1つのアプリに閉じているのでroot指定で行くのだと理解しました。
サービス経由でデータを取得
サービス提供側
import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
@Injectable({
providedIn: 'root'
})
export class HeroService {
constructor() { }
getHeroes(): Hero[] {
return HEROES;
}
}
- mockデータ(HEROES)を取得するgetterメソッドを定義
サービス利用側
import { Component } from '@angular/core';
- import { HEROES } from '../mock-heroes';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent {
// HEROESの取得はHeroServiceに委託。
// コンポーネント側で値の取得を行う必要はない。
- heroes = HEROES;
+ heroes: Hero[] = [];
selectedHero?: Hero;
+ constructor(private heroService: HeroService) {}
+ ngOnInit(): void {
+ this.getHeroes();
+ }
+ getHeroes(): void {
+ this.heroes = this.heroService.getHeroes();
+ }
...
}
主な変更点は以下の通り。
- サービス経由でHeroesオブジェクトの配列(現時点ではモックデータHEROES)を取りに行く
- コンポーネント側からのサービスへの依存注入およびそれの利用
ngOnInitライフサイクルフック
getHeroes() はコンストラクターでも呼び出すことはできますが、これは最適な方法ではありません。
コンストラクターではプロパティ定義などの簡単な初期化のみを行い、それ以外は 何もするべきではありません 。 もちろん、実際の データ取得サービスが行うであろう、サーバーへのHTTPリクエストを行う関数は呼び出すべきではありません。
getHeroes() はコンストラクターではなく、 ngOnInit ライフサイクルフック 内で呼び出しましょう。 この ngOnInit() は、 Angular が HeroesComponent インスタンスを生成した後、適切なタイミングで呼び出されます。
要は、Javaのコンストラクタで言うところのフィールドへの値のセットのみにとどめるべきで、
public class MyClass {
private String str;
public MyClass(final String str) {
this.str = str;
}
...
}
それ以外の、たとえば外部サーバへの通信などはコンストラクタ内ではやるべきではない……ということですね。
そして、そうした値のセット以外の初期化処理についてはAngularではngOnInitでやれと書いています。
コンポーネントのライフサイクルについては コンポーネントのライフサイクル で言及されています。
非同期処理: Observable
非同期処理をするときが使いどころ。
チュートリアルでは「メソッドをたたいて値が返ってくる」というのを現時点で同期的にやっています。
重たい処理をしている訳ではないので今はこれでいいのですが、例えばHTTP通信するときも同期処理をしていると、レスポンスが返ってくるまでブラウザがロックされてしまう(待ちっぱなしになってしまう)ということが発生します。
それは避けたいところです。
そこで処理を非同期にするとよく、そのために使われるのがObserbableだと書かれています。
この方法は、非同期呼び出しを使用する実際のアプリケーションではうまくいきません。 今は、サービスが同期的に モックヒーロー を返すので、うまくいっています。
もし getHeroes() がヒーローのデータをすぐに返すことができないのであれば、それは同期的であるべきではありません。 なぜなら、データを返すのを待つ間にブラウザがブロックされてしまうからです。
HeroService.getHeroes() は何らかの非同期処理を実装する必要があります。
この章では、HeroService.getHeroes() は Observable を返します。
サービス提供側
import { Observable, of } from 'rxjs';
...
export class HeroService {
...
getHeroes(): Observable<Hero[]> {
const heroes = of(HEROES);
return heroes;
}
}
- RxJS ライブラリからObservableとofをimport
- getHeroesメソッドがObservableを返すようになる
- ofを使うことで、データ(この例の場合Hero型の配列)をObservableに変換する
Observableを返すgetHeroesメソッドを使うことで、利用者側は非同期にデータを取得できる。
サービス利用側
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
- Observableを返すメソッドを利用する側は、取得したデータをsubscribeする必要がある(subscribeはObservableが提供しているメソッド)
heroes => this.heroes = heroes
は、サービスからデータheroes
が届いたら=>
、それをコンポーネントのthis.heroesに代入するthis.heroes = heroes
ということをしています。
非同期のミソ
- サービス提供側
- Observable
- 非同期処理するときに使う
- rxjsからimport
- of
- データをObservableに変換
- rxjsからimport
- Observable
- サービス利用側
- subscribe
- サービス提供側からデータを受け取る
- subscribe
チュートリアルでは、さらにサービス(MessageService)とコンポーネント(MessageComponent)を作って実装して行っていますが新しいテクニックはここに書いたもので全てです。