なになれ

IT系のことを記録していきます

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メソッドが実行される流れです。

  1. データを削除するためにComponentクラスのdeleteHeroメソッドが呼ばれる
  2. HeroServiceクラスのdeleteメソッドが呼ばれる
  3. BehaviorSubjectのnextメソッドが呼ばれる
  4. 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

bosyu.me

個人的にはリクエスト毎にOGPを設定したページを生成できるのかが疑問で、その疑問を解消する過程でWebアプリを作りました。
OGPページを作れるアプリです。
ogp-generator.hi1280.com

アプリの説明、使い方はこちらです。


OGPページ生成ツールの使い方

アプリの仕組み

実装はAngular+Firebaseでこんな形にしています。
f:id:hi1280:20180821210203p:plain

考えてみれば簡単だったんですが、サーバサイドで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アドレスをセットしています。

証明書を取得する

これからの手順を行うにあたり、前回割り当てられたIngressIPアドレスドメイン名を登録しておく必要があります。
事前にドメイン登録業者などで設定しておきます。

ドメイン名が有効になったら、証明書を取得します。

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で証明書の発行者のリソースを指定しています。
ドメイン名はcommonNamednsNamesdomainsに指定します。
ingressで前回作成したIngressのリソース名を指定します。

$ kubectl apply -f certificate.yml

ここで時間がかかりますので、5分から10分ほど待ちます。

webapp-tlsというSecretリソースが表示されれば、正常に完了しています。

$ kubectl get secrets

describeコマンドでリソース作成の進行状況を確認することができます。

$ kubectl describe -f certificate.yaml

HTTPSに対応する

証明書を使うようにIngressを更新します。

ingress-tls.yml

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

ドメイン名やSecret名をtls追記しています。

$ kubectl apply -f ingress-tls.yml

10分ほどでHTTPSでアクセスできるようになります。

まとめ

実運用に耐えうるコンテナ環境ができることをKubernetesを利用して確認できました。
今回はGKEを利用したので楽ができました。
逆に自前運用だと大変そうだと思いました。

参考にした資料

github.com