Angularにおける状態管理をRxJSで試した
状態管理はフロントエンドで必要な機能としてよく取り上げられています。
ReactにおけるReduxが有名で、Reduxライクな状態管理のライブラリが各種フレームワークで用意されていて、それらを使うのが主流なようです。
AngularでもReduxライクなngrxというライブラリがあります。
ただ自分はReduxの経験がないので、パッと見で複雑そうな印象がありました。
ngrx以外の手段で状態管理を試してみました。
どのように状態管理を行うか
参考にしたのはこちらです。 slides.com coryrylan.com
「RxJSを活用したAngularアプリの状態管理設計」では状態の同期を取るという方法が紹介されています。
これを参考にして、今回はPersistent stateとServer stateを同期しつつ、stateをComponentで扱うことを試します。
stateの同期にはRxJSの機能であるBehaviorSubjectを利用することが紹介されています。
BehaviorSubjectを使えば、任意のタイミングでデータを流すことができてかつ、流れたデータを保持することが可能です。
実装
Angularの公式チュートリアルでおなじみのTour of Heroesを元にしました。
github.com
hero.service.ts
を変更して、Server stateと同期するPersistent stateを実装しました。
hero.service.ts
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, throwError as observableThrowError, BehaviorSubject, of } from 'rxjs'; import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { Hero } from './hero'; @Injectable() export class HeroService { private _heroes: BehaviorSubject<Hero[]>; private heroes: Observable<Hero[]>; private heroesUrl = 'app/heroes'; // URL to web api constructor(private http: HttpClient) { this._heroes = new BehaviorSubject([]); } fetchHeroes(): Observable<Hero[]> { return this.http.get<Hero[]>(this.heroesUrl) .pipe( switchMap(data => { this._heroes.next(data); this.heroes = this._heroes.asObservable(); return this.heroes; }) ); } getHeroes(): Observable<Hero[]> { return this.heroes || this.fetchHeroes(); } getHero(id: number): Observable<Hero> { return this.getHeroes().pipe( map(heroes => heroes.find(hero => hero.id === id)) ); } save(hero: Hero) { if (hero.id) { return this.put(hero); } return this.post(hero); } delete(hero: Hero) { const url = `${this.heroesUrl}/${hero.id}`; return this.http.delete<Hero>(url) .pipe( tap(() => { const current = this._heroes.getValue().filter(h => h !== hero); this._heroes.next(current); }), catchError(this.handleError) ); } // Add new Hero private post(hero: Hero) { return this.http .post<Hero>(this.heroesUrl, hero) .pipe( tap((h) => { const current = this._heroes.getValue(); this._heroes.next([...current, h]); }), catchError(this.handleError) ); } // Update existing Hero private put(hero: Hero) { const url = `${this.heroesUrl}/${hero.id}`; return this.http .put<Hero>(url, hero) .pipe( tap(() => { const current = this._heroes.getValue() .map(h => { if (h.id === hero.id) { h = hero; } return h; }); this._heroes.next(current); }), catchError(this.handleError) ); } private handleError(res: HttpErrorResponse | any) { console.error(res.error || res.body.error); return observableThrowError(res.error || 'Server error'); } }
CRUDに当たる各メソッドでBehaviorSubjectのオブジェクトである_heroes
プロパティにnextメソッドでデータを流しています。
Serverへリクエストを発行するとともにそのデータを_heroes
に保持することで、Server stateとPersistent stateを同期しています。
また、BehaviorSubjectではnextメソッドを実行したタイミングでsubscribeメソッドが呼ばれるので、ComponentからPersistent stateを参照するのも簡単です。
heroes.component.ts
... ngOnInit(): void { this.getHeroes(); } getHeroes(): void { this.heroService .getHeroes() .subscribe( heroes => this.heroes = heroes, error => this.error = error ) } deleteHero(hero: Hero, event: any): void { event.stopPropagation(); this.heroService.delete(hero).subscribe(res => { if (this.selectedHero === hero) { this.selectedHero = null; } }, error => (this.error = error)); } ...
BehaviorSubjectのsubscribeメソッドが実行される流れです。
- データを削除するためにComponentクラスのdeleteHeroメソッドが呼ばれる
- HeroServiceクラスのdeleteメソッドが呼ばれる
- BehaviorSubjectのnextメソッドが呼ばれる
- Componentクラスで定義した
this.heroService.getHeroes().subscribe
の処理が呼ばれる
結果的に削除後のデータがBehaviorSubjectによってComponentクラスのheroes
プロパティからも参照されます。
まとめ
BehaviorSubjectが有能でした。
BehaviorSubjectが保持するデータとServerのデータを同期するようにServiceクラスを作り、ComponentクラスはそのBehaviorSubjectからのみデータを取得するように設計すれば、単方向のデータフローが実現できて、シンプルな状態管理を実現できそうです。
OGPページ生成ツールというWebアプリをつくった
最近Twitter等でOGPによるシェアを前提としたWebサービスが注目されています。
ちょっと前だと、Peing(質問箱)が有名で、質問がOGPでシェアされてます。
peing.net
最近だと、ためしがき、bosyuといったサービスですね。リクエスト毎にOGPでコンテンツをシェアしています。
tameshigaki.jp
個人的にはリクエスト毎にOGPを設定したページを生成できるのかが疑問で、その疑問を解消する過程でWebアプリを作りました。
OGPページを作れるアプリです。
ogp-generator.hi1280.com
アプリの説明、使い方はこちらです。
アプリの仕組み
実装はAngular+Firebaseでこんな形にしています。
考えてみれば簡単だったんですが、サーバサイドでHTMLを生成すればよかったんですね。
実装当初はHTMLファイルをStorageに保存してみたいな無駄なことをしてました。
細かい作りは以下のようにしています。
- Firebase HostingとFunctionsを接続する
- FunctionsでExpressを利用してHTMLを動的に返す
- Expressの利用は必須ではないですが、HTMLのテンプレートを楽に使えたりするので利用しました
まとめ
最近はサーバサイドでHTMLをレンダリングすることがほとんどなくなってきたので、その発想が真っ先に出てこなかった。
FIrebaseの使い方として、サーバサイドでの動的コンテンツの生成をFunctionsに任せてしまうというのは汎用性がありそうな利用法だと思いました。Expressをそのまま動かせるのは楽です。
MEAN StackをKubernetesで動かす(その2)
前回はHTTPでアクセスするところまでを行いました。
hi1280.hatenablog.com
今回はHTTPS対応を行います。
MEAN Stackのプログラム一式はこちら
github.com
実運用に耐えうることを想定して、以下の内容を含めています。
MongoDBの内容が前回までで、今回はHTTPS対応の話になります。
- MongoDBは可用性を高めるためにReplica Setで構成する
- 外部公開を考慮して、HTTPS対応を行う
cert-managerというKubernetes上で自動的にSSL証明書を管理してくれるパッケージを使います。
github.com
なお、cert-managerはまだ安定版ではないようなので、今回の内容が無効になる可能性があります。
事前のインストール
cert-managerをインストールするためにHelmというKubernetesのパッケージ管理ツールが必要です。
各環境に応じたHelmのクライアント環境をインストールします。
docs.helm.sh
KubernetesのクラスタにHelmとcert-managerをインストールします。
$ kubectl create serviceaccount -n kube-system tiller $ kubectl create clusterrolebinding tiller-binding \ --clusterrole=cluster-admin \ --serviceaccount kube-system:tiller $ helm init --service-account tiller $ helm repo update $ helm install --name cert-manager --namespace kube-system stable/cert-manager
cert-managerにおけるLet's Encryptのセットアップ
自分のemailアドレスを環境変数にセットします。
$ export EMAIL=xxx@yyy.com
証明書の発行者をLet's Encryptにしてリソースを作成します。
$ curl -sSL https://rawgit.com/ahmetb/gke-letsencrypt/master/yaml/letsencrypt-issuer.yaml | \ sed -e "s/email: ''/email: $EMAIL/g" | \ kubectl apply -f-
先ほどのemailアドレスをセットしています。
証明書を取得する
これからの手順を行うにあたり、前回割り当てられたIngressのIPアドレスにドメイン名を登録しておく必要があります。
事前にドメイン登録業者などで設定しておきます。
ドメイン名が有効になったら、証明書を取得します。
certificate.yml
apiVersion: certmanager.k8s.io/v1alpha1 kind: Certificate metadata: name: webapp-tls namespace: default spec: secretName: webapp-tls issuerRef: name: letsencrypt-prod kind: ClusterIssuer commonName: mean-k8s-example.hi1280.site dnsNames: - mean-k8s-example.hi1280.site acme: config: - http01: ingress: webapp domains: - mean-k8s-example.hi1280.site
issuerRef
で証明書の発行者のリソースを指定しています。
ドメイン名はcommonName
、dnsNames
、domains
に指定します。
ingress
で前回作成したIngressのリソース名を指定します。
$ kubectl apply -f certificate.yml
ここで時間がかかりますので、5分から10分ほど待ちます。
webapp-tls
というSecretリソースが表示されれば、正常に完了しています。
$ kubectl get secrets
describe
コマンドでリソース作成の進行状況を確認することができます。
$ kubectl describe -f certificate.yaml
HTTPSに対応する
証明書を使うようにIngressを更新します。
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: webapp labels: app: webapp spec: backend: serviceName: svc1 servicePort: 80 tls: - secretName: webapp-tls hosts: - mean-k8s-example.hi1280.site
$ kubectl apply -f ingress-tls.yml
10分ほどでHTTPSでアクセスできるようになります。
まとめ
実運用に耐えうるコンテナ環境ができることをKubernetesを利用して確認できました。
今回はGKEを利用したので楽ができました。
逆に自前運用だと大変そうだと思いました。