HttpInterceptorの使用例メモ

AngularのHttpInterceptorを使った際のメモ

# HttpInterceptor

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

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

日本語だと@ponday さんの記事がわかりやすいです。
[Angular 4.3で追加されたHttpClientModuleについてのメモ](https://qiita.com/ponday/items/1ec0e500cd801286845e#xsrf%E5%AF%BE%E7%AD%96)

# リクエストの共通設定

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

```ts
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してメッセージ表示」とかやってられないときに。

```ts
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);
      })
    );
  }
}
```

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

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

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

```ts
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型に自動変換しましょう。

```ts
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の良いところですね！