angular

HttpInterceptorの使用例メモ

Angular 4.3からのHttpInterceptorを使った際のメモ

HttpInterceptor

リクエストを飛ばす前やレスポンスを受け取った後に共通の処理を仕込めるものです。

詳細は公式ドキュメントを参照(英語)。
https://angular.io/guide/http#advanced-usage

日本語だと@ponday さんの記事がわかりやすいです。
Angular 4.3で追加されたHttpClientModuleについてのメモ

リクエストの共通設定

ヘッダーを変更したりクッキーを使う場合はリクエストをclone()して上書きします。

import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs/Rs';

@Injectable()
export class RequestInterceptor implements HttpInterceptor {

  baseUrl = 'http://localhost:8080/sample-api';

  constructor() { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const req = request.clone({
      url: `${this.baseUrl}/${request.url}`,
      withCredentials: true,
      setHeaders: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest',
      }
    });
    return next.handle(req);
  }
}

URL設定をインターセプタに移すことで、サービスからURLへの依存を引きはがせます。テストが書きやすくなりますね。

エラーメッセージ表示の自動化

「毎回手動でエラーをcatchしてメッセージ表示」とかやってられないときに。

import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs/Rx';

// サービスは別途用意
import { ToastService } from './toast.service';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

  constructor(private toastService: ToastService) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const req = request.clone();
    return next.handle(req).catch(res => {
      switch (res.status) {
        case 400:
        case 401:
        case 403:
        case 404:
        case 500:
          // {"errors":[{"message":"Sorry, that page does not exist","code":34}]}
          const errors = JSON.parse(res.errors);
          if (errors) {
            errors.forEach(error => {
              this.toastService.error(error.message, error.code);
            });
          }
          break;
        default:
          this.toastService.error('APIサーバに接続できません');
          break;
      }
      return Observable.throw(res);
    });
  }
}

トースト表示は自作でも良いですし ngx-toastr を使っても良いです。

プログレスバーの自動表示

プログレスバーの表示/非表示を手動で制御するのは割と面倒なのでインターセプタで自動化しましょう。

import { Injectable } from '@angular/core';
import { HttpEvent, HttpEventType, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';

import { Observable } from 'rxjs/Rx';

// サービスは別途用意
import { ProgressService } from './progress.service';

@Injectable()
export class ProgressInterceptor implements HttpInterceptor {

  constructor(private progressService: ProgressService) { }

  // ↓参考
  // https://github.com/MurhafSousli/ngx-progressbar/blob/master/src/services/interceptor.provider.ts
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    this.progressService.show();
    return next.handle(request)
      .do(event => {
        if (event.type === HttpEventType.UploadProgress) {
          const percent = Math.round(100 * event.loaded / event.total);
          this.progressService.set(percent);
        }
      })
      .finally(() => {
        this.progressService.hide();
      });
  }
}

( ´ー`) .oO(スピナーも同じようにできるけど画面がちらつきそう) ※ 対策はコメント参照

ISO-8601文字列 → Date型変換

レスポンスが日付を「文字列」で返す場合、そのままだとgetFullYearとか使えなくて辛い。。。
そんなときはインターセプタで文字列抽出してDate型に自動変換しましょう。

import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Rx';

/**
 * 元ネタは↓のAngularJS用インターセプタ
 * https://gist.github.com/ScottGuymer/9994dae637bb2055d58b
 */
const ISO_8601 = /^\d{4,5}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(([+-]\d\d:\d\d)|Z)?$/i;
function convertDateStringsToDates(input: any) {
  if (typeof input !== 'object') {
    return input;
  }
  for (const key in input) {
    if (!input.hasOwnProperty(key)) {
      continue;
    }
    const value = input[key];
    let match;
    if (typeof value === 'string' && (match = value.match(ISO_8601))) {
      const milliseconds = Date.parse(match[0]);
      if (!isNaN(milliseconds)) {
        input[key] = new Date(milliseconds);
      }
    } else if (typeof value === 'object') {
      convertDateStringsToDates(value);
    }
  }
}

@Injectable()
export class DateInterceptor implements HttpInterceptor {

  constructor() { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const req = request.clone();
    return next.handle(req).do(event => {
      if (event instanceof HttpResponse) {
        convertDateStringsToDates(event.body);
      }
    });

  }
}

レスポンスの加工ができるのがHttpInterceptorの良いところですね!