いちろう’s blog

すーぱーえんじにあ

Angularメモ RESTクライアントの作成

Angularチュートリアルの個人的なメモです。

6章を元にAPIからデータを取得するRESTクライアントの作成と、その際に利用するRxJSについてまとめました。

HTTP通信に利用するパッケージ

HTTTP通信の際はhttpClientを利用する。これもデフォルトで用意されているので、改めてインストールコマンドを実行する必要はない。src/app/app.module.tsに以下の行を追加する。

import { HttpClientModule } from '@angular/common/http';

importしたらsrc/app/app.module.tsAppModuleのimport配列に追加する。

@NgModule({
  imports: [
    HttpClientModule,
  ],
})

RESTfulなAPIの実行

RESTfulなAPIサーバに対して、フロント側からリクエストを投げる処理をsrc/app/hero.service.tsに記載していく。チュートリアル用のAPIサーバはangular-in-memory-web-apiを利用して構築する。

angular-in-memory-web-apiの部分は今回のメインではないので割愛するが、少量の記述で簡単にモックサーバを構築できるので、使い方を覚えておいて損はなさそう

GET

Getリクエストを利用する時はHttpClient.get<T>(${リクエスト対象})を利用する。返り値はobserverableな結果を受け取る。

  getHeroes(): Observable<Hero[]> {
    return this.http.get<Hero[]>(this.heroesUrl).pipe(
      tap(heroes => this.log('fetched heroes')), // Web画面のlogに出力 
      catchError(this.handleError<Hero[]>('getHeroes', [])) // チュートリアルで作成した処理
    )
  }

リクエストを取得するだけであれば.pipe~以降の記述は必要ないが、リクエストがエラーになった場合のエラーハンドリングやログ出力を行う場合に、pipe()で拡張するする。

tap()コールバック

Observableな変数に入力された値を元に処理を行うもの。処理結果の値を書き換えたりはせず、読み取るのみ。

RxJSにはtap()意外にも様々なコールバックようの関数が存在している。他の関数に関しては以下の記事を参考。

POST

Postリクエストを利用する時はHttpClient.post<T>(${リクエスト対象}, ${リクエストボディ}, ${リクエストヘッダー})を利用する。返り値はobserverableな結果を受け取る。

  addHero( hero: Hero ): Observable<Hero>{
    return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
      tap((newHero: Hero ) => this.log(`added hero id=${newHero.id}`)),
      catchError(this.handleError<Hero>(`addHero`))
    )
  }

HTTPヘッダーはHttpHeadersを利用し、Service内部で以下のように指定する。

import { HttpClient, HttpHeaders } from '@angular/common/http';

~~~省略~~~
  httpOptions = {
    headers: new HttpHeaders({ 'Content-Type': 'application/json' })
  };

PUT

Putリクエストを利用する時はHttpClient.put(${リクエスト対象}, ${リクエストボディ}, ${リクエストヘッダー})を利用する。返り値はobserverableな結果を受け取る。

  updatedHero(hero: Hero): Observable<any> {
    return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
      tap(_ => this.log(`updated hero id=${hero.id}`)),
      catchError(this.handleError<any>(`updateHero`))
    )
  }

DELETE

Deleteリクエストを利用する時はHttpClient.delete<T>(${リクエスト対象}, ${リクエストヘッダー})を利用する。返り値はobserverableな結果を受け取る。

  deleteHero( id: Number ):  Observable<Hero>{
    const url = `${this.heroesUrl}/${id}`;

    return this.http.delete<Hero>(url, this.httpOptions).pipe(
      tap(_ => this.log(`deleted hero id=${id}`)),
      catchError(this.handleError<Hero>(`deleteHero`))
    )
  }

deleteを利用する場合のみ、返却されたObservableに対してsubscribe()をする必要がある。subscribe()を忘れるとサーバに処理が返されず、削除処理が終了しない。

なのでdeleteHeroを呼び出す上位のサービス(src/app/heroes/heroes.component.ts)では以下の用に記載する。

  delete(hero: Hero): void {
    this.heroes = this.heroes.filter(h => h !== hero);
    this.heroService.deleteHero(hero.id).subscribe(); // ←subscribe必須!
  }

検索処理 & Observableの利用

最後に検索処理を実装する。検索処理では検索ボックスに値が入力されたら、即座に検索結果を返すようなリアルタイム検索を実装する。

リアルタイム検索の実装の際に、単純に文字の入力のたびに毎回リクエストをAPIに送ると、APIサーバに負荷がかかってしまう。それをRxJSオブジェクトを利用することで、リクエスト間隔を長くしたり、入力の文字列が変わった時だけリクエストを送る、といった制御を簡単に実装することができる。

まずsrc/app/hero.service.tsに検索のAPIをリクエストする処理を記載する。GetRequestを用いる。

searchHeroes(term: string): Observable<Hero[]> {
  if (!term.trim()) {
    // 検索語がない場合、空のヒーロー配列を返す
    return of([]);
  }
  return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
    tap(_ => this.log(`found heroes matching "${term}"`)),
    catchError(this.handleError<Hero[]>('searchHeroes', []))
  );
}

次に上記の検索リクエストを呼び出すコンポーネント(src/app/hero-search/hero-search.component.ts)を記述する。ここでRxJSを利用し、状態の制御を行う。

import { Component, OnInit } from '@angular/core';

import { Observable, Subject } from 'rxjs';

import {
   debounceTime, distinctUntilChanged, switchMap
 } from 'rxjs/operators';

import { Hero } from '../hero';
import { HeroService } from '../hero.service';

@Component({
  selector: 'app-hero-search',
  templateUrl: './hero-search.component.html',
  styleUrls: [ './hero-search.component.css' ]
})
export class HeroSearchComponent implements OnInit {
  heroes$!: Observable<Hero[]>;
  private searchTerms = new Subject<string>();

  constructor(private heroService: HeroService) {}

  // 検索語をobservableストリームにpushする
  // 何か文字が入力される度に、searchTermsにその値が代入される①
  search(term: string): void {
    this.searchTerms.next(term);
  }

  ngOnInit(): void {
    this.heroes$ = this.searchTerms.pipe(
      // ①で入力後、入力された単語を検索する前に300ms、次の入力を待つ。
      debounceTime(300),

      // 直前の検索語と同じ場合は無視する
      distinctUntilChanged(),

      // 検索語が変わる度に、新しい検索結果をObservableに代入する。
      switchMap((term: string) => this.heroService.searchHeroes(term)),
    );
  }
}

普通に実装しようとすると複雑になりやすい処理が、フレームワークに含まれるライブラリを用いることで簡単に実装ができた。