angular

HttpInterceptorの使用例メモ

Angularの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';

@Injectable()
export class RequestInterceptor implements HttpInterceptor {
  private baseUrl = 'http://localhost:8080/sample-app';

  constructor() { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // '/api'から始まるリクエスト
    if (request.url.match(/^\/api\//)) {
      const req = request.clone({
        url: `${this.baseUrl}/${request.url}`,  // 接続先URL付加
        withCredentials: true,                  // Cookie有効
        setHeaders: {                           // ヘッダー書き換え
          'Content-Type': 'application/json',
          'X-Requested-With': 'XMLHttpRequest',
        },
      });
      return next.handle(req);
    }
    return next.handle(request);
  }
}

サービスからURL設定への依存を引きはがすとテストが書きやすくなります。

URLの変更についてはHttpInterceptorではなくProxyを使用しても良いです。
https://angular.io/guide/build#proxying-to-a-backend-server

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

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

import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { MatSnackBar } from '@angular/material';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

  constructor(private toastService: MatSnackBar) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const req = request.clone();
    return next.handle(req).pipe(
      catchError(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.map(e => {
                this.toastService.open(`${e.code}: ${e.message}`);
              });
            }
            break;
          default:
            break;
        }
        return throwError(res);
      })
    );
  }
}

トースト表示は自作でも良いです。

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

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

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

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

@Injectable()
export class ProgressbarInterceptor implements HttpInterceptor {

  constructor(private progressbar: ProgressbarService) { }

  // ↓参考
  // https://github.com/MurhafSousli/ngx-progressbar/blob/master/src/services/interceptor.provider.ts
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    this.progressbar.show();
    return next.handle(request).pipe(
      //tap(event => {
      //  if (event.type === HttpEventType.UploadProgress) {
      //    const percent = Math.round(100 * event.loaded / event.total);
      //    this.progressbar.set(percent);
      //  }
      //}),
      finalize(() => {
        this.progressbar.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';
import { tap } from 'rxjs/operators';

/**
 * 元ネタは↓の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).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          convertDateStringsToDates(event.body);
        }
      })
    );
  }
}

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