Angularチュートリアルその7 HTTP

Angularをチュートリアルを使って勉強する機会があったので、その時の内容について説明していきます。

Angularの本家サイトのチュートリアルの「HTTP」の説明をしていきたいと思います。

今回のチュートリアルを行うことで、HTTPを利用した通信を行うことができるようになります。

本家サイトはこちらになります。

作成するプロジェクトは「Tour of Heroes」というアプリケーションです。

作成するアプリの概要についてはこちらから確認できます。

では、早速始めていきたいと思います!

開発環境

  • macOS Sierra 10.12.6
  • node 8.4.0
  • npm 5.3.0
  • Angular 4.3.6

HTTPサービスを有効化する

HTTPサービスを有効にするために、Angularの通信メカニズムであるHttpClientを利用します。

下記の手順でHTTPサービスを有効にします。

  • AppModuleを開く。
  • HttpClientModuleをインポートします。
  • @NgModule.importsに追加する。

編集後のapp.module.tsは下記のようになります。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import {FormsModule} from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';

import { HeroService } from './hero.service';
import { MessagesComponent } from './messages/messages.component';
import { MessageService } from './message.service';
import { AppRoutingModule } from './/app-routing.module';
import { DashboardComponent } from './dashboard/dashboard.component';

@NgModule({
  declarations: [
    AppComponent,
    HeroesComponent,
    HeroDetailComponent,
    MessagesComponent,
    DashboardComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [ HeroService, MessageService ],
  bootstrap: [AppComponent]
})
export class AppModule { }

データサーバをシミュレートする

今回のチュートリアルでは実際にデータサーバを構築することはせずに、インメモリー型のWebAPIモジュールを利用します。

下記コマンドを実行して、In-memory Web APIパッケージをインストールします。

npm install angular-in-memory-web-api --save

InMemoryWebApiModuleInMemoryDataServiceapp.module.tsにインポートします。

import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService }  from './in-memory-data.service';

app.module.ts@NgModule.importsInMemoryDataServiceの設定を記述します。

  imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule,
    HttpClientModule,
    // The HttpClientInMemoryWebApiModule module intercepts HTTP requests
    // and returns simulated server responses.
    // Remove it when a real server is ready to receive requests.
    HttpClientInMemoryWebApiModule.forRoot(
      InMemoryDataService, { dataEncapsulation: false }
    )
  ],

in-memory-data.service.tsを新規に作成します。

import { InMemoryDbService } from 'angular-in-memory-web-api';

export class InMemoryDataService implements InMemoryDbService {
  createDb() {
    const heroes = [
      { id: 11, name: 'Mr. Nice' },
      { id: 12, name: 'Narco' },
      { id: 13, name: 'Bombasto' },
      { id: 14, name: 'Celeritas' },
      { id: 15, name: 'Magneta' },
      { id: 16, name: 'RubberMan' },
      { id: 17, name: 'Dynama' },
      { id: 18, name: 'Dr IQ' },
      { id: 19, name: 'Magma' },
      { id: 20, name: 'Tornado' }
    ];
    return {heroes};
  }
}

サービスとHTTP

hero.service.tsHTTPClientをインポートします。

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

constructorの部分も編集します。

constructor(
  private http: HttpClient,
  private messageService: MessageService) { }

logメソッドを追加します。

/** Log a HeroService message with the MessageService */
private log(message: string) {
  this.messageService.add('HeroService: ' + message);
}

heroesUrlプロパティを追加します。

private heroesUrl = 'api/heroes';  // URL to web api

HttpClientを利用してデータを取得する

hero.service.tsgetHeroesメソッドを編集します。

/** GET heroes from the server */
getHeroes (): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl)
}

エラーハンドリング

サーバ通信時のイレギュラーに対応するためのエラーハンドリングを行います。

hero.service.tsに下記のインポートを追加します。

import { catchError, map, tap } from 'rxjs/operators';

getHeroesメソッドにエラーハンドリングの処理を組み込みます。

getHeroes (): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl)
    .pipe(
      catchError(this.handleError('getHeroes', []))
    );
}

handleError

hero.service.tshandleErrorメソッドを追加します。

/**
 * Handle Http operation that failed.
 * Let the app continue.
 * @param operation - name of the operation that failed
 * @param result - optional value to return as the observable result
 */
private handleError<T> (operation = 'operation', result?: T) {
  return (error: any): Observable<T> => {

    // TODO: send the error to remote logging infrastructure
    console.error(error); // log to console instead

    // TODO: better job of transforming error for user consumption
    this.log(`${operation} failed: ${error.message}`);

    // Let the app keep running by returning an empty result.
    return of(result as T);
  };
}

getHeroesメソッド実行時にログを出力できるように修正します。

/** GET heroes from the server */
getHeroes (): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl)
    .pipe(
      tap(heroes => this.log(`fetched heroes`)),
      catchError(this.handleError('getHeroes', []))
    );
}

idを利用してデータを取得する

idを利用して対象データを取得します。

hero.service.tsgetHeroメソッドを編集します。

/** GET hero by id. Will 404 if id not found */
getHero(id: number): Observable<Hero> {
  const url = `${this.heroesUrl}/${id}`;
  return this.http.get<Hero>(url).pipe(
    tap(_ => this.log(`fetched hero id=${id}`)),
    catchError(this.handleError<Hero>(`getHero id=${id}`))
  );
}

データの保存

データの保存処理を実装していきます。

hero-detail.component.htmlsaveボタンを追加します。

<button (click)="save()">save</button>

hero-detail.component.tssaveメソッドを追加します。

save(): void {
   this.heroService.updateHero(this.hero)
     .subscribe(() => this.goBack());
 }

HeroServiceにupdateHeroを追加する

hero.service.tsupdateHeroメソッドを追加します。

/** PUT: update the hero on the server */
updateHero (hero: Hero): Observable<any> {
  return this.http.put(this.heroesUrl, hero, httpOptions).pipe(
    tap(_ => this.log(`updated hero id=${hero.id}`)),
    catchError(this.handleError<any>('updateHero'))
  );
}

httpOptions定数をhero.service.tsに追加します。

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

データの追加

データの追加処理を実装していきます。

heroes.component.htmlにデータ追加用のフォームを記述します。

<div>
  <label>Hero name:
    <input #heroName />
  </label>
  <!-- (click) passes input value to add() and then clears the input -->
  <button (click)="add(heroName.value); heroName.value=''">
    add
  </button>
</div>

heroes.component.tsaddメソッドを追加します。

add(name: string): void {
  name = name.trim();
  if (!name) { return; }
  this.heroService.addHero({ name } as Hero)
    .subscribe(hero => {
      this.heroes.push(hero);
    });
}

HeroServiceにaddHeroを追加する

hero.service.tsaddHeroメソッドを追加します。

/** POST: add a new hero to the server */
addHero (hero: Hero): Observable<Hero> {
  return this.http.post<Hero>(this.heroesUrl, hero, httpOptions).pipe(
    tap((hero: Hero) => this.log(`added hero w/ id=${hero.id}`)),
    catchError(this.handleError<Hero>('addHero'))
  );
}

データの削除

データの削除処理を実装していきます。

heroes.component.htmlに削除ボタンを配置します。

<button class="delete" title="delete hero"
(click)="delete(hero)">x</button>

heroes.component.tsdeleteメソッドを追加します。

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

HeroServiceにdeleteHeroを追加する

hero.service.tsdeleteHeroメソッドを追加します。

/** DELETE: delete the hero from the server */
deleteHero (hero: Hero | number): Observable<Hero> {
  const id = typeof hero === 'number' ? hero : hero.id;
  const url = `${this.heroesUrl}/${id}`;

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

名前でデータを検索する

名前でデータを検索するためのsearchHeroesメソッドをhero.service.tsに追加します。

/* GET heroes whose name contains search term */
searchHeroes(term: string): Observable<Hero[]> {
  if (!term.trim()) {
    // if not search term, return empty hero array.
    return of([]);
  }
  return this.http.get<Hero[]>(`api/heroes/?name=${term}`).pipe(
    tap(_ => this.log(`found heroes matching "${term}"`)),
    catchError(this.handleError<Hero[]>('searchHeroes', []))
  );
}

ダッシュボード画面に検索機能を追加する

dashboard.component.htmlに検索フォームを追加します。

<h3>Top Heroes</h3>
<div class="grid grid-pad">
  <a *ngFor="let hero of heroes" class="col-1-4"
      routerLink="/detail/{{hero.id}}">
    <div class="module hero">
      <h4>{{hero.name}}</h4>
    </div>
  </a>
</div>

<app-hero-search></app-hero-search>

HeroSearchComponentを新規作成する

下記コマンドを実行してHeroSearchComponentを追加します。

ng generate component hero-search

hero-search.component.htmlを編集します。

<div id="search-component">
  <h4>Hero Search</h4>

  <input #searchBox id="search-box" (keyup)="search(searchBox.value)" />

  <ul class="search-result">
    <li *ngFor="let hero of heroes$ | async" >
      <a routerLink="/detail/{{hero.id}}">
        {{hero.name}}
      </a>
    </li>
  </ul>
</div>

HeroSearchComponentクラスを確定する

hero-search.component.tsを下記のように修正します。

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

import { Observable } from 'rxjs/Observable';
import { Subject }    from 'rxjs/Subject';
import { of }         from 'rxjs/observable/of';

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) {}

  // Push a search term into the observable stream.
  search(term: string): void {
    this.searchTerms.next(term);
  }

  ngOnInit(): void {
    this.heroes$ = this.searchTerms.pipe(
      // wait 300ms after each keystroke before considering the term
      debounceTime(300),

      // ignore new term if same as previous term
      distinctUntilChanged(),

      // switch to new search observable each time the term changes
      switchMap((term: string) => this.heroService.searchHeroes(term)),
    );
  }
}

完成イメージ

チュートリアルが完了するとこんな感じで動きます。

ソースコードについて

今までのソースコードはGithubにあげてますので、詳細を確認したい方はこちらからソースコードを見てもらえればと思います。

最後に

今回のチュートリアルでHTTP通信を利用したサーバとの通信が行えるようになりました。今回のチュートリアルでAngularのチュートリアルは全て完了となりました。

今後、発展的な実装方法などあれば随時情報を展開していきたいと思います!!