サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Angular 5入門 05: データをサービスにより提供する


Angular 5入門 04: データのリストを表示する」は、データのリスト表示と詳細情報の編集を別のコンポーネントに切り分けました。本稿は、さらにデータの取得や提供などの管理を、サービスのモジュールに分けます。そうすると、コンポーネントはデータがどこからどのように得られているのか気にすることなく、もっぱら表示と入力を扱えばよくなります。モジュールの役割は絞られ、管理がしやすくなるのです。

Angular Version 6については「Angular 6入門 05: データをサービスにより提供する」をお読みください。

01 サービスのモジュールをつくる

まず、サービスのモジュールをAngular CLIでつくります。コマンドラインツールでアプリケーションのディレクトリ(angular-tour-of-heroines)に移ったら、つぎのようにng generate serviceコマンドを打ち込んでください。サービスのモジュール(heroine.service)がつくられ、以下のようなひな形のクラス(HeroineService)が定められます。クラスのサービスを他のモジュールから受けられるようにるのが、デコレータ関数Injectable()です。

ngコマンド

ng generate service heroine

src/app/heroine.service.ts

import {Injectable} from '@angular/core';
@Injectable()
export class HeroineService {
	constructor() {}
}

サービスのモジュール(heroine.service)のクラス(HeroineService)には、つぎのTypeScriptコードのとおり、データ提供のメソッド(getHeroines())を書き加えます。コンポーネントはこのメソッドによりデータを得て、共有することができるのです。

src/app/heroine.service.ts

import {Heroine} from './heroine';
import {HEROINES} from './mock-heroines';

export class HeroineService {

	getHeroines(): Heroine[] {
		return HEROINES;
	}
}

デコレータ関数Injectable()を添えたサービスのクラス(HeroineService)は、使う側のクラスにNgModule()関数で引数オブジェクトのprovidersプロパティに配列要素として加えなければなりません。今回は、つぎのようにアプリケーションのモジュール(app.module)に組み込みます。こうすると、アプリケーションのコンポーネントはすべてそのサービスが受けられるようになるのです(「Providers」の「Provider scope」参照)。providersプロパティの配列に入れたサービスのインスタンスはひとつだけつくられ、用いるクラスが共有する仕組みになっています(「Services」の「Provide the HeroService」参照)。

src/app/app.module.ts

import {HeroineService} from './heroine.service';

@NgModule({

	providers: [HeroineService],

})
export class AppModule {}

なお、サービスのモジュール(heroine.service)をつくるときに、組み込み先がアプリケーションモジュールと決まっていた場合、ng generate serviceコマンドにつぎの--moduleオプションを添えれば上記のimportprovidersの定めはAngular CLIが加えてくれます。

ngコマンド

ng generate service heroine --module=app

02 コンポーネントからサービスのメソッドを呼び出す

前項でアプリケーションモジュールに組み込んだサービス(heroine.service')のメソッドを、リスト表示のコンポーネント(heroines.component)から呼び出しましょう。以下のように、項目データの定数(HEROINES)に替えて、サービスのクラス(HeroineService)をimportすると、サービスのインスタンスがコンストラクタ(constructor())の引数に渡されます(「Angular2のDIを知る」参照)。その参照をprivateのプロパティ(heroineService)に納めました。privateはクラスの外からのアクセスを許さないTypeScriptの修飾子です。コンストラクタの引数にアクセス修飾子を添えると、プロパティの宣言が兼ねられます(「TypeScript: クラス」03-04「引数でプロパティを定める」)。

そして、新たに加えたメソッド(getHeroines())が、サービスのインスタンス(heroineService)のメソッド(getHeroines())からデータを受け取って、みずからのプロパティ(heroines)に定めています。メソッドを呼び出すのは、コンポーネントがつくられたすぐあとに実行されるメソッドngOnInit()の本体がよいでしょう(ngOnInit()については「Angular 5入門 02: 編集ページをつくる」01「新たなコンポーネントをつくる」参照)。

src/app/heroines/heroines.component.ts

// import {HEROINES} from '../mock-heroines';
import {HeroineService} from '../heroine.service';

export class HeroinesComponent implements OnInit {
	// heroines = HEROINES;
	heroines: Heroine[];

	constructor(private heroineService: HeroineService) {}
	ngOnInit(): void {
		this.getHeroines();
	}

	getHeroines(): void {
		this.heroines = this.heroineService.getHeroines();
	}
}

これで、サービスのモジュールが分けられました。前回「Angular 5入門 04」のコード(「Angular 5 Example - Tour of Heroines 04」)と動きは変わりません。データがリスト表示され、マウスクリックで選んだ項目の詳細情報が示されます(図001)。

図001■リストから選んだ項目の詳細情報が示される

図001

切り分けたサービス(heroine.service)とそれを組み込んだリスト表示コンポーネント(heroines.component)それぞれのTypeScriptコード全体はつぎのコード001にまとめました。

コード001■サービスとリスト表示コンポーネントのTypeScriptコード

src/app/heroine.service.ts

import {Injectable} from '@angular/core';
import {Heroine} from './heroine';
import {HEROINES} from './mock-heroines';
@Injectable()
export class HeroineService {
	constructor() {}
	getHeroines(): Heroine[] {
		return HEROINES;
	}
}
 
src/app/heroines/heroines.component.ts

import {Component, OnInit} from '@angular/core';
import {Heroine} from '../heroine';
import {HeroineService} from '../heroine.service';
@Component({
	selector: 'app-heroines',
	templateUrl: './heroines.component.html',
	styleUrls: ['./heroines.component.css']
})
export class HeroinesComponent implements OnInit {
	heroines: Heroine[];
	selectedHeroine: Heroine;
	constructor(private heroineService: HeroineService) {}
	ngOnInit(): void {
		this.getHeroines();
	}
	onSelect(heroine: Heroine): void {
		this.selectedHeroine = heroine;
	}
	getHeroines(): void {
		this.heroines = this.heroineService.getHeroines();
	}
}

03 Observableを使って非同期に処理する

リスト表示コンポーネント(heroines.component)は、サービス(heroineService)のメソッド(getHeroines())の戻り値をプロパティ(heroines)に与えました。サービスのクラス(HeroineService)に定められたメソッドは、データのモジュール(mock-heroines)から得た定数(HEROINES)の配列をそのまま返しています。これらの処理は同期(synchronous)で行われているのです。

src/app/heroines/heroines.component.ts

export class HeroinesComponent implements OnInit {

	constructor(private heroineService: HeroineService) {}

	getHeroines(): void {
		this.heroines = this.heroineService.getHeroines();
	}
}

src/app/heroine.service.ts

import {HEROINES} from './mock-heroines';

export class HeroineService {

	getHeroines(): Heroine[] {
		return HEROINES;
	}
}

けれど、データをモジュール(mock-heroines)でなくサーバーから受け取ろうとすると、レスポンスを待たなければならず、同期でデータは得られません。処理を非同期で組み立てなければならないのです。つぎの3つのやり方が考えられます。

  1. コールバックを呼び出す
  2. Promiseを使う
  3. Observableを使う

今回は、RxJSObservableを使うことにします(RxJSとObservableについては「RxJS を学ぼう #1 - これからはじめる人のための導入編」参照)。AngularのHTTPからデータを得るHttpClient.get()メソッドObservableオブジェクトを返すからです。サービスのモジュール(heroine.service)は、つぎのようにRxJSのObservableクラスとof()関数(RxJSでは「オペレータ」と呼びます)をimportします。of()は引数をObservableオブジェクトにするオペレータです(「RxJS を学ぼう #2 - よく使う ( と思う ) オペレータ15選」の「Observable.of」参照)。メソッドの戻り値がObservableの場合の型づけは、非同期処理が解決されたときの型をジェネリック<>で添えます。

src/app/heroine.service.ts

import {Observable} from 'rxjs/Observable';
import {of} from 'rxjs/observable/of';

export class HeroineService {
	constructor() {}
	// getHeroines(): Heroine[] {
	getHeroines(): Observable<Heroine[]> {
		// return HEROINES;
		return of(HEROINES);
	}
}

サービス(heroine.service)のメソッド()から返されるオブジェクトが変わりましたので、呼び出すコンポーネント(heroines.component)のメソッド(getHeroines())もつぎのように書き替えます。Observableオブジェクトが解決されたときの処理は、Observable.subscribe()メソッドの引数にコールバック関数として定めます。コールバックは解決されたオブジェクトを引数に受け取るので、それをプロパティ(heroines)に定めればよいでしょう。関数はアロー関数式=>で書きました。引数ひとつであれば丸かっこ()は外してよく、本体が一文なら波かっこ{}も省けるのです。

src/app/heroines/heroines.component.ts

export class HeroinesComponent implements OnInit {

	getHeroines(): void {
		// this.heroines = this.heroineService.getHeroines();
		this.heroineService.getHeroines()
		.subscribe(heroines => this.heroines = heroines);  
	}
}

これでアプリケーションのデータが非同期で得られるようになりました。動きは相変わらず変わっていません(図002)。今はまだ、データをモジュール(mock-heroines)から読み込んでいます。けれど、サーバーからでもデータを受け取れる仕組みが整ったのです。サービスのモジュール(heroine.service)とリスト表示コンポーネント(heroines.component)のTypeScriptは、以下のコード002に全体を掲げました。

図002■読み込んだデータのリストから選んだ項目が編集できる

図002

コード002■サービスとリスト表示コンポーネントのTypeScriptコード

src/app/heroine.service.ts

import {Injectable} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {of} from 'rxjs/observable/of';
import {Heroine} from './heroine';
import {HEROINES} from './mock-heroines';
@Injectable()
export class HeroineService {
	constructor() {}
	getHeroines(): Observable<Heroine[]> {
		return of(HEROINES);
	}
}

src/app/heroines/heroines.component.ts

import {Component, OnInit} from '@angular/core';
import {Heroine} from '../heroine';
import {HeroineService} from '../heroine.service';
@Component({
	selector: 'app-heroines',
	templateUrl: './heroines.component.html',
	styleUrls: ['./heroines.component.css']
})
export class HeroinesComponent implements OnInit {
	heroines: Heroine[];
	selectedHeroine: Heroine;
	constructor(private heroineService: HeroineService) {}
	ngOnInit(): void {
		this.getHeroines();
	}
	onSelect(heroine: Heroine): void {
		this.selectedHeroine = heroine;
	}
	getHeroines(): void {
		this.heroineService.getHeroines()
		.subscribe(heroines => this.heroines = heroines);  
	}
}

04 メッセージを加えるサービス

サービスのモジュールをもうひとつ加えます。アプリケーションの処理をメッセージとして加え、それらが取り出せるサービスです。つぎのng generate serviceコマンドでサービスをつくるとともに、--moduleオプションでアプリケーションに設定します。

ngコマンド

ng generate service message --module=app

つくられたひな形のモジュール(message.service)のTypeScriptコードはつぎのとおりです。

src/app/message.service.ts

import {Injectable} from '@angular/core';
@{Injectable()
export class MessageService {
	constructor() {}
}

メッセージのサービスのクラス(MessageService)には、つぎのようにメッセージを納める配列のプロパティ(messages)および、メッセージの追加(add())と消去(clear())のメソッドを書き加えます。

src/app/message.service.ts

export class MessageService {
	messages: string[] = [];
	// constructor() {}
	add(message: string) {
		this.messages.push(message);
	}
	clear() {
		this.messages = [];
	}
}

メッセージのサービス(message.service)はつぎのように親のサービス(heroine.service)にimportして、クラス(HeroineService)のコンストラクタでインスタンスをprivateなプロパティ(messageService)に定めます。そして、親のデータを渡すメソッド(getHeroines())が呼び出されたとき、メッセージのサービスの追加メソッド(add())でメッセージを加えることにしましょう。確認のため、console.log()メソッドで、メッセージサービスのオブジェクトをコンソールに書き出しています。

src/app/heroine.service.ts

import {MessageService} from './message.service';

export class HeroineService {
	// constructor() {}
	constructor(private messageService: MessageService) {}
	getHeroines(): Observable<Heroine[]> {
		this.messageService.add('HeroineService: データを取得');
		console.log(this.messageService);  // 確認用
		return of(HEROINES);
	}
}

アプリケーションを読み込むと、ブラウザのコンソールにつぎのようなかたちでメッセージサービスのインスタンスの中身が示されるはずです。これで、TypeScriptコードが動いたことは確かめられました。メッセージ(message.service)と親(heroine.service)のサービスのTypeScriptコードの全体は以下のコード003のとおりです。

MessageService {messages: Array(1)}
	messages: ["HeroineService: データを取得"]
	__proto__: Object

コード003■メッセージと親のサービスのTypeScriptコード

src/app/message.service.ts

import {Injectable} from '@angular/core';
@Injectable()
export class MessageService {
	messages: string[] = [];
	add(message: string) {
		this.messages.push(message);
	}
	clear() {
		this.messages = [];
	}
}

src/app/heroine.service.ts

import {Injectable} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {of} from 'rxjs/observable/of';
import {Heroine} from './heroine';
import {HEROINES} from './mock-heroines';
import {MessageService} from './message.service';
@Injectable()
export class HeroineService {
	constructor(private messageService: MessageService) {}
	getHeroines(): Observable<Heroine[]> {
		this.messageService.add('HeroineService: データを取得');
		return of(HEROINES);
	}
}

05 メッセージを表示するコンポーネント

メッセージサービス(message.service)に加えられたメッセージを取り出して、アプリケーションのページに示しましょう。つぎのng generate componentコマンドで、新たなコンポーネント(messages.component)をつくります。でき上がるひな形のTypeScriptコードは以下のとおりです。

ngコマンド

ng generate component messages

src/app/messages/messages.component.ts

import {Component, OnInit} from '@angular/core';
@Component({
	selector: 'app-messages',
	templateUrl: './messages.component.html',
	styleUrls: ['./messages.component.css']
})
export class MessagesComponent implements OnInit {
	constructor() { }
	ngOnInit(): void {
	}
}

リスト表示のコンポーネント(app.component)のテンプレートに、メッセージコンポーネント(messages.component)のタグ(app-messages)を加えます。親となるリスト表示と子のメッセージのコンポーネントのテンプレートは、それぞれ以下のコード004のとおりです。

メッセージコンポーネントのテンプレートは、ngForディレクティブで、プロパティ(messages)の配列に納められたメッセージをすべて取り出して、バインディング表示します(オブジェクトmessageServiceは、このあとコンポーネントのTypeScriptコードに加えます)。ただし、メッセージがない(Array.lengthプロパティが0)ときは、ngIfディレクティブにより要素のノードはつくられません(ディレクティブngForngIfについては、それぞれ「Angular 5入門 03」の02「複数データをリスト表示する」と03「リストからクリックした項目のデータを編集する」参照)。また、ボタン(<button>要素)クリックでメッセージを消去(clear())します。

コード004■リスト表示とメッセージのコンポーネントのテンプレート

src/app/app.component.html

<h1>{{title}}</h1>
<app-heroines></app-heroines>
<app-messages></app-messages>

src/app/messages/messages.component.html

<div *ngIf="messageService.messages.length">
	<h2>メッセージ</h2>
	<button class="clear" (click)="messageService.clear()">消去</button>
	<div *ngFor='let message of messageService.messages'>{{message}}</div>
</div>

メッセージコンポーネント(messages.component)のTypeScriptコードには、つぎのようにメッセージサービスのクラス(MessageService)をimportしてください。すると、コンストラクタがサービスのインスタンスを引数に受け取りますので、publicのプロパティ(messageService)として宣言します。publicとしたのは、前掲のテンプレートでバインディングするためです。

src/app/messages/messages.component.ts

import {MessageService} from '../message.service';

export class MessagesComponent implements OnInit {
	// constructor() {}
	constructor(public messageService: MessageService) {}

}

でき上がったメッセージコンポーネント(messages.component)のTypeScriptコードはつぎのコード005のとおりです。以下のCSSファイルを定めるとスタイルが整います。また、Angular CLIで新たなサービスとコンポーネントが組み込まれたアプリケーションモジュール(app.module)のTypeScriptコードも併せて掲げました。Plunkerのサンプルコードは「Angular 5 Example - Tour of Heroines 05」をご覧ください。

コード005■メッセージコンポーネントのTypeScriptコードとCSSファイルおよびアプリケーションモジュールのTypeScriptコード

src/app/messages/messages.component.ts

import {Component, OnInit} from '@angular/core';
import {MessageService} from '../message.service';
@Component({
	selector: 'app-messages',
	templateUrl: './messages.component.html',
	styleUrls: ['./messages.component.css']
})
export class MessagesComponent implements OnInit {
	constructor(public messageService: MessageService) {}
	ngOnInit(): void {
	}
}

src/app/messages/messages.component.css

h2 {
	color: red;
	font-family: Arial, Helvetica, sans-serif;
	font-weight: lighter;
}
body {
	margin: 2em;
}
body, input[text], button {
	color: crimson;
	font-family: Cambria, Georgia;
}
button.clear {
	font-family: Arial;
	background-color: #eee;
	border: none;
	padding: 5px 10px;
	border-radius: 4px;
	cursor: pointer;
	cursor: hand;
}
button:hover {
	background-color: #cfd8dc;
}
button:disabled {
	background-color: #eee;
	color: #aaa;
	cursor: auto;
}
button.clear {
	color: #888;
	margin-bottom: 12px;
}

src/app/app.module.ts

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {AppComponent} from './app.component';
import {HeroinesComponent} from './heroines/heroines.component';
import {HeroineDetailComponent} from './heroine-detail/heroine-detail.component';
import {HeroineService} from './heroine.service';
import {MessagesComponent} from './messages/messages.component';
import {MessageService} from './message.service';
@NgModule({
	declarations: [
		AppComponent,
		HeroinesComponent,
		HeroineDetailComponent,
		MessagesComponent
	],
	imports: [
		BrowserModule,
		FormsModule
	],
	providers: [HeroineService, MessageService],
	bootstrap: [AppComponent]
})
export class AppModule {}


作成者: 野中文雄
作成日: 2018年1月24日


Copyright © 2001-2018 Fumio Nonaka.  All rights reserved.