AngularにおけるRxJSのObservable
Angularの学習を公式のチュートリアルに即しながら、わかりづらい部分を補足しながら進めています。
前回はAngularのサービスについてと学びました。今回は非同期処理用のライブラリRxJSとそのクラスObservableの使い方を学びます。合わせてシングルトンやジェネリクス(Generics)についても触れています。
RxJS
Webアプリを構築する際に非同期処理(終了を待たない処理)は必須といえるでしょう。
Javascriptの非同期処理にはPromiseを使ったものがありますが、チュートリアルではObservableを使っています。
ObservableはRxJSという非同期処理ライブラリのクラスです。
Promiseとの違いは「Observable と 他の技術の比較」に掲載されています。
Observable
テストデータのHEROESを非同期処理で取得したのと同じように扱えるよう修正します。そうすることであとから非同期処理を実装した際に簡単に差し替えができるようになります。
前回作成したhero.service.tsを次のように修正します。このファイルはsrc¥appに作成したものでした。
この時RxJSライブラリから、Observableと ofをインポートします。関数のgetHeroes()はObsarvableクラスを返すように設定しています。この時の<>の部分はGenericsと言って、変数の型を可変に受け取ることができるものです。ここではObservableにHero[]を指定しています。
HEROESはRxJSのof関数によってObservableクラスに変換されます。
hero.service.ts
import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class HeroService {
constructor() { }
getHeroes(): Observable<Hero[]> {
return of(HEROES);
}
}
次にheroes.compornet.tsを次のように書き換えます。
heroes.compornet.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
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;
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
constructor(private heroService: HeroService) { }
ngOnInit(): void {
this.getHeroes();
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
}
視覚的には何も変わりませんが、これでデータが戻ってくるのを待つことができます。Obserbableのsubscribeメソッドはデータを待機し受信した後、第1引数に渡した関数を実行します。
subscribe内のアロー関数を従来の記述方法に直すと次のようになります。thisの値を変えないように.bind(this)を付けています。
引数に渡す関数では先ほど<>内に指定した型の値を受け取れます。ここではHero配列が1件戻ってくるだけなので1度しか関数が呼ばれませんが、複数件戻ってくる場合は関数はデータの終わりまで繰り返し実行されます。
subscribeメソッドは(next,error,complete)と3つの引数を持つことができます。nextは前述のデータの件数だけ繰り返し実行される関数、errorは処理がエラーとなった時に実行される関数、completeはすべてのデータを処理し終えたとき実行される関数です。errorとcompleteはオプションです。
Observableの詳細
以上のような感じでAngularではRxJSを利用するのですが、これだけでは説明不足だと思いますので、公式ページに沿ってもう少し詳しく解説します。
Angularではパブリッシャーと呼ばれるデータを送信する側と、サブクライバーと呼ばれる受信側を定義しています。その仲介をするのがRxJSのObservableオブジェクトです。
先ほどはof関数を使って簡単にObservableオブジェクトを作成しましたが、今度はもっと基本的な方法であるnewを使った方法で作成してみます。
Observableオブジェクトを作成するときコンストラクタに関数を渡します。関数ではobserverというオブジェクトを引数に受け取ります。
ここで受けるobserverはJavaでいうインタフェースのような扱いで、そのオブジェクトには「next」という値を取り出す処理、「error」というエラーを発生させる処理、「complete」というすべての処理の完了を告げる処理がある前提となっています。
このobserverの処理の実装はデータ受信の際に指定することができます。Javaでいうならobserverインターフェースをimplementしたインスタンスを渡します。Observableオブジェクトを生成する段階では、それらのobserverに備わった処理(メソッド)をどう呼び出すのかを記述します。そして、最後に処理を中断する際の関数を含んだオブジェクトを返します。
参考ページに書かれているコードを転載します。Geolocationは、一部のブラウザで HTTPS接続環境で利用できる位置情報を取得するAPIです。それがユーザーエージェントの情報を記憶しているnavigatorに存在するかをチェックして存在した際に通常の処理に進み、そうでなければobserverのエラー処理を呼びます。
通常の処理中では、geolocation.watchPositionでは位置情報が変わる毎に呼び出す関数を第1引数に、エラー時に呼び出す関数を第2引数に設定します。ここでのエラー処理でもobserver.errorを呼びます。
geolocation.watchPositionからデータが発行された場合はobserverのnextにその値を渡して呼び出します。
最後に終了処理用の関数を含んだオブジェクトを戻り値として返します。
geolocation.watchPositionはそのままだとずっと位置情報の監視を続けます。それを終わらせるためgeolocation.clearWatchを呼部処理を終了処理用の関数で記述しています。
const locations = new Observable((observer) => {
let watchId: number;
if ('geolocation' in navigator) {
watchId = navigator.geolocation.watchPosition((position: Position) => {
observer.next(position);
}, (error: PositionError) => {
observer.error(error);
});
} else {
observer.error('Geolocation not available');
}
return {
unsubscribe() {
navigator.geolocation.clearWatch(watchId);
}
};
});
subscribeの詳細
先の記述で作成されたObservableオブジェクトはそのままの状態では何も処理をしません。このことは、オブジェクトを生成した時点でと処理を始めるPromiseとの違いのひとつです。
Observableオブジェクトのsubscribeを呼び出した時に処理が開始されます。この時subscribeには実装済みのobserverオブジェクトを渡します。
subcribeにobserverを渡すには(next関数,error関数,complete関数)としてもいいですし、({next(param){処理}})や({next: (param) => {処理}})という風にオブジェクトとして渡してもいいです。先にも書きましたが、errorとcompleteはオプションなので処理内で呼び出されなければなくてもかまいません。
公式のサンプルコードでは次のように続いています。これは単に位置情報やエラーをconsoleに出力するだけのものです。locationsSubscriptionには終了処理用のオブジェクトが帰ってきます。それをsetTimeoutを使って10秒後に呼び出しています。
const locationsSubscription = locations.subscribe({
next(position) {
console.log('Current Position: ', position);
},
error(msg) {
console.log('Error Getting Location: ', msg);
}
});
// 終了処理
setTimeout(() => {
locationsSubscription.unsubscribe();
}, 10000);
ofを使った先のコードをnewを使って作成すすると次のようになります。
...
getHeroes(): Observable<Hero[]> {
return new Observable((observer){
observer.next(HEROES);
observer.cmplete();
return { unsubscribe() {}};
});
}
...
ofは可変に引数を取り引で指定した順にnextで呼び出すObservableオブジェクトを作成する関数です。ここではオブジェクトがまとめて返るようになっていますが、順にデータを渡したければof(HEROES[0],HEROES[1],...)という風にします。ちなみに配列やiterableオブジェクトの中身を順に読みだすObservableを作成する関数にfromがあります。
シングルトンの確認
チュートリアルアプリの読解へ戻ります。
チュートリアルアプリにメッセージを表示するmessagesコンポーネント、そのデータ処理をするmessageサービスを追加します。
これらは、今までの作業の繰り返しになりますが、次のチュートリアルで該当箇所が出現するので記述しておきます。
このとき、どのからメッセージを追加しても意図したように表示されるのは、messageサービスのインスタンスが一つである(シングルトンである)ことを示しています。
次のチュートリアルのための準備
以下、追加・修正項目を足早に紹介しておきます。
src¥appでmessagesコンポーネントと、messageサービスを作成します。
PS> ng generate service message
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 = [];
}
}
新しく作成されたmessageフォルダの、messages.component.tsとmessages.component.htmlを編集します。Messageサービスはコンポーネントから直接利用するのでpublicにします。
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 {
}
}
messages.component.html(全て記述し直し)
<div *ngIf="messageService.messages.length">
<h2>Messages</h2>
<button class="clear"
(click)="messageService.clear()">clear</button>
<div *ngFor='let message of messageService.messages'> {{message}} </div>
</div>
メインページでメッセージを表示させるためにsrc¥appフォルダにある、app.compornent.htmlでapp-massages要素を追加します。
app.compornent.html
<h1>{{title}}</h1>
<app-heroes></app-heroes>
<app-messages></app-messages>
messageはhero.service.tsとheroes.component.tsで発信することにします。それぞれ次のようになります。先ほどはpulicで設定しましたが、これらの定義では外側からMessageServiceを利用しないためprivateにしておきます。
hero.service.ts
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { MessageService } from './message.service';
@Injectable({
providedIn: 'root',
})
export class HeroService {
constructor(private messageService: MessageService) { }
getHeroes(): Observable<Hero[]> {
this.messageService.add('HeroService: fetched heroes');
return of(HEROES);
}
}
heroes.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
import { MessageService } from '../message.service';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
selectedHero: Hero;
heroes: Hero[];
constructor(private heroService: HeroService, private messageService: MessageService) { }
ngOnInit() {
this.getHeroes();
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
this.messageService.add(`HeroesComponent: Selected hero id=${hero.id}`);
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
}