HTML5テクニカルノート
Angular 6入門 05: データをサービスにより提供する
- ID: FN1805009
- Technique: HTML5 / JavaScript
- Package: Angular 6.0.0
「Angular 6入門 04: データのリストを表示する」は、データのリスト表示と詳細情報の編集を別のコンポーネントに切り分けました。本稿は、さらにデータの取得や提供などの管理を、サービスのモジュールに分けます。そうすると、コンポーネントはデータがどこからどのように得られているのか気にすることなく、もっぱら表示とデータのやりとりを扱えばよくなります。モジュールの役割は絞られ、管理がしやすくなるのです。
01 サービスのモジュールをつくる
まず、サービスのモジュールをAngular CLIでつくります。コマンドラインツールでプロジェクトのディレクトリ(angular-tour-of-heroines)に移ったら、つぎのようにng generate service
コマンドを打ち込んでください。サービスのモジュール(heroine.service)がつくられ、以下のようなひな形のクラス(HeroineService)が定められます。クラスのサービスを他のモジュールから受けられるようにるのが、デコレータ関数Injectable()
です。
ngコマンドng generate service heroine
src/app/heroine.service.tsimport {Injectable} from '@angular/core'; @Injectable({ providedIn: 'root' }) export class HeroineService { constructor() {} }
サービスのモジュール(heroine.service)のクラス(HeroineService)には、つぎのTypeScriptコードのとおり、データ提供のメソッド(getHeroines())を書き加えます。コンポーネントはこのメソッドによりデータを得て、共有することができるのです。
src/app/heroine.service.tsimport {Heroine} from './heroine'; import {HEROINES} from './mock-heroines'; export class HeroineService { getHeroines(): Heroine[] { return HEROINES; } }
サービスを他のコンポーネントから使うには、プロバイダに加えなければなりません。プロバイダがサービスのインスタンスをつくって、Angularのアプリケーションに提供するのです。
デコレータ関数Injectable()
のメタデータprovidedIn
を見ると、Angular CLIコマンドによりサービスがroot
のプロバイダに登録されています。これにより、アプリケーションのコンポーネントすべてが、そのサービスを受けられるようになるのです(「Providers」の「Provider scope」参照)。サービスのインスタンスはひとつだけつくられ、用いるクラスが共有する仕組みになっています(「Services」の「Provide the HeroService」参照)。
src/app/heroine.service.ts@Injectable({ providedIn: 'root' }) export class HeroineService { }
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 6入門 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 6入門 04」のコード(「angular-6-example-tour-of-heroines-04」)と動きは変わりません。データがリスト表示され、マウスクリックで選んだ項目の詳細情報が示されます(図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({
providedIn: 'root'
})
export class HeroineService {
constructor() {}
getHeroines(): Heroine[] {
return HEROINES;
}
}
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.tssrc/app/heroine.service.tsexport class HeroinesComponent implements OnInit { constructor(private heroineService: HeroineService) {} getHeroines(): void { this.heroines = this.heroineService.getHeroines(); } }
import {HEROINES} from './mock-heroines'; export class HeroineService { getHeroines(): Heroine[] { return HEROINES; } }
けれど、データをモジュール(mock-heroines)でなくサーバーから受け取ろうとすると、レスポンスを待たなければならず、同期でデータは得られません。処理を非同期で組み立てなければならないのです。つぎの3つのやり方が考えられます。
- コールバックを呼び出す
Promise
を使うObservable
を使う
今回は、RxJSのObservable
を使うことにします。AngularのHTTPからデータを得るHttpClient.get()
メソッドがObservable
オブジェクトを返すからです。サービスのモジュール(heroine.service)は、つぎのようにRxJSのObservable
クラスとof()
関数(RxJSでは「オペレータ」と呼びます)をimport
します。of()
は引数をObservable
オブジェクトにするオペレータです(「RxJS を学ぼう #2 - よく使う ( と思う ) オペレータ15選」の「Observable.of」参照)。メソッドの戻り値がObservable
の場合の型づけは、非同期処理が解決されたときの型をジェネリック<>
で添えます。
src/app/heroine.service.tsimport {Observable, of} from 'rxjs'; 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.tsexport 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■サービスとリスト表示コンポーネントのTypeScriptコード
src/app/heroine.service.ts
import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
import {Heroine} from './heroine';
import {HEROINES} from './mock-heroines';
@Injectable({
providedIn: 'root'
})
export class HeroineService {
constructor() {}
getHeroines(): Observable<Heroine[]> {
return of(HEROINES);
}
}
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
つくられたひな形のモジュール(message.service)のTypeScriptコードはつぎのとおりです。
src/app/message.service.tsimport {Injectable} from '@angular/core'; @Injectable({ providedIn: 'root' }) export class MessageService { constructor() {} }
メッセージのサービスのクラス(MessageService)には、つぎのようにメッセージを納める配列のプロパティ(messages)および、メッセージの追加(add())と消去(clear())のメソッドを書き加えます。
src/app/message.service.tsexport 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.tsimport {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({
providedIn: 'root'
})
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}
import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
import {Heroine} from './heroine';
import {HEROINES} from './mock-heroines';
import {MessageService} from './message.service';
@Injectable({
providedIn: 'root'
})
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.tsimport {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
ディレクティブにより要素のノードはつくられません(ディレクティブngFor
とngIf
については、それぞれ「Angular 6入門 03」の02「複数データをリスト表示する」と03「リストからクリックした項目のデータを編集する」参照)。また、ボタン(<button>
要素)クリックでメッセージを消去(clear())します。
コード004■リスト表示とメッセージのコンポーネントのテンプレート
src/app/app.component.html
<h1>{{title}}</h1>
<app-heroines></app-heroines>
<app-messages></app-messages>
<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
としたのは、前掲のテンプレートでバインディングするためです(public
でないコンポーネントプロパティはバインディングできません)。
src/app/messages/messages.component.tsimport {MessageService} from '../message.service'; export class MessagesComponent implements OnInit { // constructor() {} constructor(public messageService: MessageService) {} }
でき上がったメッセージコンポーネント(messages.component)のTypeScriptコードはつぎのコード005のとおりです。以下のCSSファイルを定めるとスタイルが整います。また、Angular CLIで新たなサービスとコンポーネントが組み込まれたアプリケーションモジュール(app.module)のTypeScriptコードも併せて掲げました。StackBlitzのサンプルコードは「angular-6-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() {
}
}
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;
}
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 {MessagesComponent} from './messages/messages.component';
@NgModule({
declarations: [
AppComponent,
HeroinesComponent,
HeroineDetailComponent,
MessagesComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
Angular 6入門
- Angular 6: Angular CLIで手早くアプリケーションをつくる
- Angular 6入門 01: アプリケーションの枠組みをつくる
- Angular 6入門 02: 編集ページをつくる
- Angular 6入門 03: データのリストを表示する
- Angular 6入門 04: 詳細情報のコンポーネントを分ける
- Angular 6入門 06: ルーティングで画面を切り替える
- Angular 6入門 07: ルーティングで個別情報を示す
- Angular 6入門 08: HTTPサービスでリモートのデータを取り出して書き替える
作成者: 野中文雄
作成日: 2018年5月15日
Copyright © 2001-2018 Fumio Nonaka. All rights reserved.