Angularのサービス
Vue.jsとAngularどちらにするか悩んでいる筆者が、それらを比較するためにAngularを学んでいます。公式のチュートリアルに即しながら、わかりづらい部分を補足しながら進めています。
今回はAngularのサービスについて理解するために、過去に作成したheroes.componentの機能を分割します。途中、コンポーネント間でプロパティの受け渡しをする場合の@Inputについても触れていますが、その詳細はリンク先の記事で紹介しているので合わせてお読みいただければ幸いです。
新たなコンポーネントの作成
heroesコンポーネントを作成した時と同様にhero-detailコンポーネントを作成します。
PS> ng generate component hero-detail
...
コマンドラインからモジュールを作成することで、app.module.tsに必要なモジュール記述が自動的に行われます。
hero-detail.component.tsファイルを次のように編集します。
hero-detail.component.ts
import { Component, OnInit, Input } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: ['./hero-detail.component.css']
})
export class HeroesComponent implements OnInit {
@input() hero: Hero;
constructor() { }
ngOnInit(): void {
}
}
親となるコンポーネントからHeroインタフェースのデータを受け取る為に、Inputをimportした後、heroに対して@input()を設定しています。ここでの@もデコレータのそれです。
hero-detail.component.htmlの方はコードの修正はありません。
hero-detail.component.html
<div *ngIf="hero">
<h2>{{hero.name | uppercase}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div>
<label>name:
<input [(ngModel)]="hero.name" placeholder="name"/>
</label>
</div>
</div>
hero.component.htmlは次のように修正します。要素を使って子コンポーネントを呼び出す時に属性として [子のプロパティ名]="親のプロパティ名" とすることで、親のプロパティを子に渡すことができます。
hero.component.html
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<app-hero-detail [hero]="selectedHero"></app-hero-detail>
これはプロパティバインディングと呼ばれます。
プロパティバインディングでは親の子への単方向バインディングになります。親での変更は子に伝えられますが、子の変更は親には伝えられません。ここではオブジェクトが渡されているので子側でプロパティを変更すると親の値が書き換わります。そのためさも双方向のような印象を受けます。
単方向の意味は、コードを次のように修正して実行するとわかりやすくなると思います。「call new hero」ボタン押下で子側でheroに対して新たなオブジェクトを設定しても、親のリストには伝わりません。
hero-detail.component.html
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
<button (click)="newHero()">call new hero</button>
</li>
</ul>
hero-detail.component.ts
import { Component, OnInit, Input } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: ['./hero-detail.component.css']
})
export class HeroesComponent implements OnInit {
@input() hero: Hero;
constructor() { }
ngOnInit(): void {
}
newHero() {
console.log('call newHero');
this.hero = {id : 20, name: 'new hero'};
}
}
サービス
Augularの構造をMVCモデルというのか、MVVMモデルというのか、それともほかの何かと呼ぶのか正確なところは筆者にはよくわかりませんが、Angularはプログラムコードは機能別分離されるべきだという基本概念の元に設計されています。そうすることで、負荷の軽減やコードの簡素化・汎用化が見込まれます。
ちなみにMVCモデルでは、アプリケーション全体を次のように分けています。
- データ取得や保存、ビジネスロジックの処理をするM(Model)部
- ユーザーにデータを表示するV(View)部
- ユーザーからの入力をモデルに伝えるC(Controller)部
これにAngularを当てはめるならテンプレートはView、コンポーネントはControllerとなります。
サービスは残ったModel、つまりデータ取得や保存、ビジネスロジックの処理をする部分にあたります。
ここまで作ってきたサンプルアプリでは、テストデータであるHEROESをコンポーネントで取得していましたが、この考えにあてはめるとデータはサービスで取得するべきです。
そこでサービスを作成し、コードを修正してデータ処理をサービスへ担わせます。
コマンドラインからappフォルダに移動し、「ng generate service サービス名」としてサービスを作成します。
PS> ng generate service hero
...
このコマンドでは、hero.service.spec.tsとhero.service.tsファイルがカレントフォルダに作成されます。
componentの時と同様にhero.service.spec.tsはテストファイルになります。
生成されたhero.service.tsを次のように修正します。HeroインターフェースとHEROES定数をimportし、HEROESを返すメソッド(関数)を定義します。
hero.service.ts
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;
}
}
プロバイダー
サービスを生成、提供する機能をプロバイダーと呼びます。
HeroesComponentに依存性の注入する(@Component内の設定を反映させる)前にプロバイダーを登録することで、注入時にサービスで取得したデータを渡すことができます。
さらにプロバイダーはインジェクターという必要な場所でプロバイダーを選択して注入するオブジェクトに登録されている必要があります。
上のコード中の@Injectable({...})の部分は、HeroServiceプロバイーダーをルートインジェクターに登録することを意味しています。
サービスの注入
サービスの注入と書くと少し難しい響きになってしまいますが、先ほどのHeroServiceを受け取ることです。
app¥heroes¥フォルダにあるheroes.component.tsのコードを次のように修正します。
heroes.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
//import { HEROES } from '../mock-heroes';削除
import { HeroService } from '../hero.service';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes: Hero[];
selectedHero: Hero;
constructor(private heroService: HeroService) { }
ngOnInit(): void {
this.getHeroes();
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}
}
constructor部の「private heroService: HeroService」の部分がサービスの注入です。構文としてはprivateプロパティとしてheroServiceをHeroServiceの型であると定義しているだけですが、こうすることでAngularはここがHeroServiceを注入する場所と認識して処理を行います。この時HeroServiceはシングルトンインスタンスになります。つまりHeroSercieのインスタンスは常にひとつだけということです。
あとはJavascriptのクラスに、メソッドを追加する要領と同じです。getHeroes()メソッドを作り、内部でheroServiceのgetHeroes()メソッドを呼び値をheroesに受け取ります。ngOnInit()メソッドにその呼び出しを書くことでインスタンス作成時にgetHeroes()を呼び出します。
constructorの{}内にも処理を書く事ができますが、(少なくとも)Angularでは簡単な初期化のみを行いそれ以外は何もするべきではないということです。
ここまで作ってきたアプリに視覚的な変化はありませんが、ここまでの記述でアプリにサービスを適用することができました。