Carpe Diem

備忘録

BehaviorSubjectでAPIの結果をキャッシュ

概要

クライアントからのAPIコールは可能であれば避けた方がサーバの負荷も下がり、ユーザの体感速度も上がります。
1度取得すればほぼ変わらないデータなどは、最初にAPIコールした後はできれば避けたいです。
一方でcookieやlocalstorageで管理するほどでもない、というときはBehaviorSubjectを利用します。

環境

  • Angular 4.3.4
  • rxjs 5.4.2

BehaviorSubjectとは

基本的な動作

BehaviorSubjectの大きな特徴は直前にonNextで渡された値を保持し、subscribe()getValue()するとその保持していた値を取得できるところです。

f:id:quoll00:20170812194105p:plain

ref: ReactiveX - Subject

サンプル

const sub = new Rx.BehaviorSubject(1);

sub.subscribe(v => {
  console.log("1st: " + v);
});
console.log('getValue1: ' + sub.getValue());

console.log('next: 10');
sub.next(10);
console.log('getValue2: ' + sub.getValue());

sub.subscribe(v => {
  console.log("2nd: " + v);
});

BehaviorSubject-001

結果

"1st: 1"
"getValue1: 1"
"next: 10"
"1st: 10"
"getValue1: 10"
"2nd: 10"

エラー時の動作

エラーが起きた時は、それ以前にsubscribeしていれば値を取得できますが、エラー後にsubscribeすると値は取得できずエラーのみ取得できます。

f:id:quoll00:20170812194131p:plain

ref: ReactiveX - Subject

サンプル

const sub = new Rx.BehaviorSubject(1);

sub.subscribe(v => {
  console.log("1st: " + v);
}, err => {
  console.log("1st error: " + err);
});

console.log('next: 10');
sub.next(10);
const error = new Error("some error");
sub.error(error);

sub.subscribe(v => {
  console.log("2nd: " + v);
}, err => {
  console.log("2nd error: " + err);
});

BehaviorSubject-002

結果

"1st: 1"
"next: 10"
"1st: 10"
"1st error: Error: some error"
"2nd error: Error: some error"

APIコールのキャッシュとしての使い方

以下のように役割を分けます。

メソッド 役割
fetch() APIコールしてデータを取得。取得後にSubjectにデータを追加
get() キャッシュがあればそれを利用。無ければfetch()に委譲

コードとしては以下のようになります。

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

import 'rxjs/add/operator/do';

import { Article } from './article';

@Injectable()
export class ArticleService {
  private articleSource: BehaviorSubject<Article> = new BehaviorSubject<Article>(new Article());

  constructor(private http: HttpClient) { }

  get(id: string): Observable<Article> {
    const article = this.articleSource.getValue();
    if (Object.keys(article).length !== 0) {
      return this.articleSource;
    }

    return this.fetch(id);
  }

  fetch(id: string): Observable<Article> {
    return this.http.get<Article>('http://jsonplaceholder.typicode.com/posts/' + id)
      .do(article => {
        this.articleSource.next(article);
      });
  }
}

まとめ

BehaviorSubjectを使うことで、リロードなどで内部オブジェクトがリセットされない限りはずっと保持できるようになり、無駄なAPIコールを減らすことができます。
Updateなどの時もfetch()の時と同様にnext()でデータを更新することで、get()時にデータがちゃんと更新されていきます。

ソース