この記事はAngular+Firebaseでチャットアプリを作るのエントリーです。
前記事:Angularのルーティング設定(基礎編)
次記事:Angular+Firebase Authenticationで認証機能を導入する
この記事で行うこと
前回の記事ではルーティングの基本的な設定を行いました。
本稿ではAngularに標準採用されているRxJSを使って、アプリ内のセッション管理をする基礎を作っていきます。
RxJSとは
『正式名称「Reactive Extensions for JavaScript」。リアクティブプログラミングを行う「Reactive extensions」のJavaScript版です。』
『リアクティブプログラミングとは、データストリームと変更の伝播に関係する非同期プログラミングの仕組みです。』
最初に私がこの説明を見た時、全く頭に入ってきませんでした。
Angularで本格的に開発を続けていこうとすると、必ずぶつかる壁がこのRxJSだと思います。
RxJSを深く理解しようとするなら専用の文献を読んだ方が良いと思われますが、2018年9月時点でRxJS専門とした国内の書籍はまだ発売されていないようです。(RxJavaの専門書は何点か出版されています。)
最近はRxJS関連の書籍もでてきたようです。
参考: https://www.techbookrank.com/booklists/5ab6d99630e841000419cd79/RxJS
ここではAngularの設計に必要な範囲で、具体的な実装をしながら解説していきます。
参考
ReactiveX(公式サイト:日本語なし)
RxJSの基本をまとめてみた~基本的な概念編(Observable、Observer、Subscriptionなど)~
RxJS を学ぼう #1 - これからはじめる人のための導入編
RxJS 5と6の変更点について
RxJSはバージョン5から6に変更されるに伴い、いくつかの破壊的変更が加えられました。それに合わせて本記事も修正を加えています。具体的な変更点は次の通りです。
- インポートパスの変更
/* RxJS 5.x */
import { Observable } from 'rxjs/Observable';
/* RxJS 6.0 */
import { Observable } from 'rxjs';
- インポートパスの変更(オペレータ)
/* RxJS 5.5 */
// まとめてインポート
import { map, filter, reduce } from 'rxjs/operators';
// 個別にインポート
import { map } from 'rxjs/operator/map';
/* RxJS 6.0 */
// こちらに統一
import { map, filter, reduce } from 'rxjs/operators';
- ifオペレータの名称変更
- 一部オペレータの削除
- Observable.neverの廃止
- Node.jsのサポート範囲変更
詳細は次の参考を参照してください。
参考
(2018/1追記)記述を現時点で最新のものに差し替えました。
(2018/9追記)記述を現時点で最新のものに差し替えました。
(2020/6追記)記述を現時点で最新のものに差し替えました。
実装内容
AngularのRxJS
RxJSを理解するにあたって、なぜRxJS(リアクティブプログラミング)が必要とされているのかを考えてみます。
リクエストのPromiseとストリームのObservable
ECMAScript2015から導入されたPromiseクラスですが、このクラスはリクエストに対するレスポンス処理を行う場面で活躍します。
よく使われるのはhttp通信などのデータ通信時です。取得したい内容をサーバー側に投げて、そのレスポンスによってその後の処理を変更する、といった振る舞いをします。
従来のWEB環境であれば、この処理だけで十分でした。しかし、リクエストしてレスポンスを受け取るという挙動は処理コストが高く、やりとりするデータ量が増えるに連れてその振る舞いを見直す必要が生じました。
この状況をふまえ、近年急速に普及しているのがサーバーpush型のデータ通信手法です。これまでクライアント側がリクエストしない限り取得できなかったデータを、サーバー側のタイミングでクライアント側に投げることでデータ通信にかかるコストを減らし、かつデータが更新された瞬間にクライアントへ反映することができるようになりました。
ですが、従来のPromiseクラスではクライアント側からリクエストしない限りデータを取得することができないため、任意のタイミングでデータを送ってくるpush型データ通信のメリットを享受することができません。
そこで登場したのが、データを観測するクラス「Observable」です。
Observableはpush型通信で送られてくるデータを流れ(ストリーム)として捉え、pushが行われたタイミングでそのデータに対し処理を行うことができます。
これによりサーバー側のデータ変更に対して柔軟に対応する(リアクティブする)アプリケーションの構築が可能になりました。
Observableのnextとsubscribe
Observableを実装する際は、データを流す(next)ことと、受け取る(subscribe)ことが必要になります。push通信で受け取ったデータを流すこともできますし、任意のタイミングで特定のデータを流すこともできます。
簡単な実装例をあげてみます。
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; // 追加
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
public loginObservable: Observable<string> = new Observable((observer => { // 追加
observer.next('test1');
setTimeout(() => {
observer.next('test2');
}, 1000);
}));
constructor() {
}
ngOnInit() {
this.loginObservable.subscribe((data: string) => { // 追加
console.log(data);
});
}
}
実行結果(コンソール)
test1
test2 // 少し遅れて表示
Observableクラスのインスタンスを作成し、observerのnextを使って値を流しています。
push通信を再現するため、2回目のnextは実行タイミングを少し遅らせてみました。
Observableクラスのインスタンスを格納したloginObservableのsubscribeでデータを受け取り、そのデータに対して処理(console.log
)を行っています。
任意のタイミングでデータを流すSubject
AngularでObservableを使うとき、もう一つ抑えておきたいクラスがあります。
上記のコードではObservableクラスのインスタンスを作成したタイミングでしかデータを流すことができず、クリックなどのイベントをトリガーとしてデータを処理したいような場合には向いていません。
そんな時に使用するのがSubjectクラスです。
SubjectクラスのインスタンスはObservableとobserverの2つの役割を同時に担うことができ、任意のタイミングでデータを流すことができます。
先ほどのコードをSubjectに修正してみます。
import { Subject } from 'rxjs'; // 変更
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
public loginSubject: Subject<number> = new Subject(); // 変更
public loginState = this.loginSubject.asObservable(); // 追加
public count = 0; // 追加
constructor() {
}
ngOnInit() {
this.loginState.subscribe((data: number) => { // 変更
console.log(data);
});
}
clickNext() { // 追加
this.count++;
this.loginSubject.next(this.count);
}
}
<!-- Subjectテスト -->
<div class="text-center">
<button class="btn text-center" (click)="clickNext()">Next</button>
</div>
loginSubjectでSubjectのインスタンスを格納し、loginStateにSubjectのObservableを格納します。Subjectのインスタンスは前出のobserverと同じ振る舞いをするため、clickNext()
が発火するたびにloginStateへデータを流していきます。
クリックするたびにデータが更新されることが確認できました。
これらをふまえて、RxJSを使ったsessionの管理を行っていきます。
※ここでの変更は確認用なので、動作検証後削除してください。
sessionサービスを作る
ではRxJSの具体的な実装に入っていきますが、その前にAngularのデータ授受の方法について確認しておきます。
コンポーネント間のデータをやりとりする方法
Angularはver2以降からコンポーネント志向の仕組みを取り入れていて、それぞれのコンポーネントが極力疎結合であることを目指しています。
なのでコンポーネント間のデータのやりとりも方法が限定されており、グローバル変数やグローバル関数を使ってデータの受け渡しをするといった従来のやり方は行うことができません。
コンポーネント間のデータ授受の方法には、次のようなものがあります。
-
@Input
、@Output
を使って親子間のデータを授受する - ルータのdataを使って、異なるルート間のデータを授受する
- サービスをDIして、異なるコンポーネント間のデータを授受する
- RxJSを使って、異なるコンポーネント間のデータを授受する
1、2については今回のテーマから外れるため、参考を示すだけに留めておきます。
3、4のデータ授受方法について解説していきます。
参考
Component Interaction
Routing & Navigation
サービスをDIして、異なるコンポーネント間のデータを授受する
セッションのようなサイトの全体で値を共有する必要があるものについては、コアモジュール内にサービスを作って共有できるようにします。
Angular5まではモジュール内のprovidersでサービスを宣言する必要がありましたが、Angular6からはprovidedIn: 'root'
という記述を加えることで、シングルトンのサービスをどこからでも参照できるようになりました。
特定のモジュール内でのみ使用したい場合は、従来通りモジュール内のprovidersでサービスを宣言する必要があります。
ng g service service/session
作成したサービスの中に、セッション用のプロパティを追加します。
import { Injectable } from '@angular/core';
import { Session } from '../../class/chat'; // 追加
@Injectable({
providedIn: 'root'
})
export class SessionService {
public session = new Session(); // 追加
constructor() { }
}
export class Session { // 追加
login: boolean;
constructor() {
this.login = false;
}
}
初期状態ではsession.login
がfalse
となり、ログイン時はこのプロパティがtrue
になることで状態を判断します。
このプロパティを他のコンポーネントで共有する場合は、DIを行ってプロパティの値を渡します。
import { Component, OnInit } from '@angular/core';
import { SessionService } from '../service/session.service'; // 追加
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {
public login: boolean; // 追加
constructor(public sessionService: SessionService) { } // 追加
ngOnInit() {
this.login = this.sessionService.session.login; // 追加
console.log('header-component-login:' + this.login); // 追加
}
}
実行結果
header-component-login:false
RxJSを使って、異なるコンポーネント間のデータを授受する
さて、それでは本題に入っていきましょう。
RxJSを使ってログインコンポーネントで変更したセッションの値を、ヘッダーコンポーネントに反映させます。
まず、sessionサービスにlogin関数とlogout関数を作成します。
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs'; // 追加
import { Router } from '@angular/router'; // 追加
import { Session } from '../../class/chat';
@Injectable({
providedIn: 'root'
})
export class SessionService {
public session = new Session();
public sessionSubject = new Subject<Session>(); // 追加
public sessionState = this.sessionSubject.asObservable(); // 追加
constructor(private router: Router) { } // 追加
login(): void { // 追加
this.session.login = true;
this.sessionSubject.next(this.session);
this.router.navigate(['/']);
}
logout(): void { // 追加
this.sessionSubject.next(this.session.reset());
this.router.navigate(['/account/login']);
}
}
export class Session {
login: boolean;
constructor() {
this.login = false;
}
reset(): Session { // 追加
this.login = false;
return this;
}
}
observerをsessionSubject、ObservableをsessionStateとして宣言し、ログイン、ログアウト時にはログインセッションを変更してストリームに流すよう設定しています。
import { Component, OnInit } from '@angular/core';
import { SessionService } from '../../core/service/session.service'; // 追加
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
constructor(private sessionService: SessionService) { } // 追加
ngOnInit() {}
submitLogin() { // 追加
this.sessionService.login();
}
}
<button class="btn btn-lg btn-info btn-block" type="submit" (click)="submitLogin()">LOGIN</button><!-- submitLogin()を追加 -->
ログインコンポーネントにsessionサービスをDIし、login関数を参照します。
ログインボタン押下時にsessionSubject(observer)のnext()
が発火し、sessionState(Observable)を通して他のコンポーネントにセッションの値を流します。
import { Session } from '../../class/chat'; // 追加
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {
public login = false; // 変更
constructor(public sessionService: SessionService) { }
ngOnInit() {
this.sessionService.sessionState.subscribe((session: Session)=> { // 追加
if (session) {
this.login = session.login;
}
});
}
logout(): void { // 追加
this.sessionService.logout();
}
}
<!-- ログイン状態で分岐 -->
<span class="navbar-text" *ngIf="!login">
<a routerLink="/account/login">Login</a>
</span>
<span class="navbar-text" *ngIf="login">
<a routerLink="/account/login" (click)="logout()">Logout</a>
</span>
<!-- 分岐ここまで -->
ヘッダーコンポーネントにもsessionサービスをDIし、sessionStateのsubscribe()
を使ってストリームに流れたデータを受け取ります。
その値をヘッダーコンポーネントのloginプロパティに割り当てることで、テンプレートにある「login」リンクをリアクティブに変化させます。
実際にアプリを起動して、ログインボタンを押してみます。
ログインボタンを押すことで、「login」リンクが「logout」リンクになることが確認できました。
最後に、この時のデータの流れを図にしたものを紹介します。
ソースコード
この時点でのソースコード
※firebaseのapiKeyは削除しているので、試すときは自身で作成したapiKeyを入れてください。