HTTP を用いて非同期で情報を取得する例 (2)

このページでは、アメリカの州の名前や州コードを検索するサービスを作り、それをバックエンドで呼び出す例を示します。

入力ボックスがあり、入力文字列をパラメータにしてバックエンドに問い合わせを行い、結果を表示します。

州の検索は次の PHP スクリプト us-states.php で行います。ベタ書きです。州名かコードに、検索文字を含むデータを返します。

<?php

$states = [
	['state'=>'Alabama', 'code'=> 'AL', 'capital'=>'Montgomery'],
	['state'=>'Alaska', 'code'=> 'AK', 'capital'=>'Juneau'],
	['state'=>'Arizona', 'code'=> 'AZ', 'capital'=>'Phoenix'],
	['state'=>'Arkansas', 'code'=> 'AR', 'capital'=>'Little Rock'],
	['state'=>'California', 'code'=> 'CA', 'capital'=>'Sacramento'],
	['state'=>'Colorado', 'code'=> 'CO', 'capital'=>'Denver'],
	['state'=>'Connecticut', 'code'=> 'CT', 'capital'=>'Hartford'],
	['state'=>'Delaware', 'code'=> 'DE', 'capital'=>'Dover'],
	['state'=>'Florida', 'code'=> 'FL', 'capital'=>'Tallahassee'],
	['state'=>'Georgia', 'code'=> 'GA', 'capital'=>'Atlanta'],
	['state'=>'Hawaii', 'code'=> 'HI', 'capital'=>'Honolulu'],
	['state'=>'Idaho', 'code'=> 'ID', 'capital'=>'Boise'],
	['state'=>'Illinois', 'code'=> 'IL', 'capital'=>'Springfield'],
	['state'=>'Indiana', 'code'=> 'IN', 'capital'=>'Indianapolis'],
	['state'=>'Iowa', 'code'=> 'IA', 'capital'=>'Des Moines'],
	['state'=>'Kansas', 'code'=> 'KS', 'capital'=>'Topeka'],
	['state'=>'Kentucky', 'code'=> 'KY', 'capital'=>'Frankfort'],
	['state'=>'Louisiana', 'code'=> 'LA', 'capital'=>'Baton Rouge'],
	['state'=>'Maine', 'code'=> 'ME', 'capital'=>'Augusta'],
	['state'=>'Maryland', 'code'=> 'MD', 'capital'=>'Annapolis'],
	['state'=>'Massachusetts', 'code'=> 'MA', 'capital'=>'Boston'],
	['state'=>'Michigan', 'code'=> 'MI', 'capital'=>'Lansing'],
	['state'=>'Minnesota', 'code'=> 'MN', 'capital'=>'St. Paul'],
	['state'=>'Mississippi', 'code'=> 'MS', 'capital'=>'Jackson'],
	['state'=>'Missouri', 'code'=> 'MO', 'capital'=>'Jefferson City'],
	['state'=>'Montana', 'code'=> 'MT', 'capital'=>'Helena'],
	['state'=>'Nebraska', 'code'=> 'NE', 'capital'=>'Lincoln'],
	['state'=>'Nevada', 'code'=> 'NV', 'capital'=>'Carson City'],
	['state'=>'New Hampshire', 'code'=> 'NH', 'capital'=>'Concord'],
	['state'=>'New Jersey', 'code'=> 'NJ', 'capital'=>'Trenton'],
	['state'=>'New Mexico', 'code'=> 'NM', 'capital'=>'Santa Fe'],
	['state'=>'New York', 'code'=> 'NY', 'capital'=>'Albany'],
	['state'=>'North Carolina', 'code'=> 'NC', 'capital'=>'Raleigh'],
	['state'=>'North Dakota', 'code'=> 'ND', 'capital'=>'Bismarck'],
	['state'=>'Ohio', 'code'=> 'OH', 'capital'=>'Columbus'],
	['state'=>'Oklahoma', 'code'=> 'OK', 'capital'=>'Oklahoma City'],
	['state'=>'Oregon', 'code'=> 'OR', 'capital'=>'Salem'],
	['state'=>'Pennsylvania', 'code'=> 'PA', 'capital'=>'Harrisburg'],
	['state'=>'Rhode Island', 'code'=> 'RI', 'capital'=>'Providence'],
	['state'=>'South Carolina', 'code'=> 'SC', 'capital'=>'Columbia'],
	['state'=>'South Dakota', 'code'=> 'SD', 'capital'=>'Pierre'],
	['state'=>'Tennessee', 'code'=> 'TN', 'capital'=>'Nashville'],
	['state'=>'Texas', 'code'=> 'TX', 'capital'=>'Austin'],
	['state'=>'Utah', 'code'=> 'UT', 'capital'=>'Salt Lake City'],
	['state'=>'Vermont', 'code'=> 'VT', 'capital'=>'Montpelier'],
	['state'=>'Virginia', 'code'=> 'VA', 'capital'=>'Richmond'],
	['state'=>'Washington', 'code'=> 'WA', 'capital'=>'Olympia'],
	['state'=>'West Virginia', 'code'=> 'WV', 'capital'=>'Charleston'],
	['state'=>'Wisconsin', 'code'=> 'WI', 'capital'=>'Madison'],
	['state'=>'Wyoming',  'code'=>'WY', 'capital'=>'Cheyenne']
];

$s = $_REQUEST['s'];
$r = [];

foreach( $states as $state ){

	if( stripos( $state['state'], $s ) !== false 
	|| stripos( $state['code'], $s ) !== false ){
		$r[] = $state;
	}
	
}

sleep(1);

echo json_encode( $r );

?>

テスト用なので、sleep で 1 秒処理を止めてます。

データモデルの作成

データモデルとして、USState (src/app/usstate.ts) を次とします。

export class USState {
  constructor(
    public StateName: string,
    public StateCode: string,
    public CapitalCity: string
  ){}
}

サービスの作成

バックエンドに問い合わせを行い、データを取得するサービス (src/app/state.service.ts) を作成します。

import {Http, Response} from '@angular/http';
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

import {USState} from './usstate'

@Injectable()
export class StateService {
  constructor(private http: Http){}

  searchState(s: string) : Observable<USState[]> {
    console.log("State::Service searchState called");
    return this.http
      .get(`http://hostname はここ/us-states.php?s=${s}`)
        .map(this.handleData)
        .catch(this.handleFailure);
  }

  handleData(response: Response) : USState[] {
    let states = response.json();
    let r = [];
    for(let i=0;i<states.length; i++){
      r.push(new USState(
        states[i].state,
        states[i].code,
        states[i].capital));
    }
    return r;
  }

  handleFailure(error: any){
    let msg: string;
    msg = "Error occurred.";
    return Observable.throw(msg);
  }

}

searchState メソッドで、検索文字列を受け取って、検索結果を返します。

以前の例では Promise を返しましたが、ここでは USState の配列の Observable です。 HTTP の get メソッドは Response の Observable です。従って get に続く、map に指定するメソッドでは Response を受け取り、 USStateの配列 (USState[]) を返しています。

catch 側については、どこで何がキャッチされるかわからないので any を受け取っています。 そして、文字列を throw しています。string であれば null でない限り変換できるので、どこかで catch したときに処理可能です。

検索フォームの作成

検索フォームコンポーネント (src/app/state-search.component.ts) は次の通りです。

import {Component, OnInit} from '@angular/core';
import {FormControl} from '@angular/forms';
import {Observable} from 'rxjs/Observable';

import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/distinctUntilChanged';

import {StateService} from './state.service';
import {USState} from './usstate';

@Component({
  selector: 'state-search',
  templateUrl: './state-search.component.html',
  providers: [StateService]
})
export class StateSearchComponent implements OnInit {

  textControl = new FormControl();

  states: USState[];
  err: string;
  search: string;
  resultNum: number;

  isLoading = false;
  didSearch = false;

  constructor(private stateService: StateService){}

  ngOnInit(){
    this.initParams();
    this.textControl.valueChanges
      .filter(s => s.length > 0)
      .debounceTime(400)
      .distinctUntilChanged()
      .subscribe(
        s => {
          this.initParams();
          this.search = s;
          this.isLoading = true;
          this.stateService.searchState(s)
          .subscribe(
            states => {
              this.isLoading = false;
              this.didSearch = true;
              this.resultNum = states.length;
              this.states = states;
            },
            err => {
              this.isLoading = false;
              this.didSearch = true;
              this.err = err.toString();
            }
          );
      });

  }

  initParams(){
    this.err = "";
    this.states = [];
    this.resultNum = 0;
    this.search = "";
  }
}

上記のサービスを呼ぶ側では、サービスをディペンデンシ・インジェクションで取り込み、 searchState が返す Observable を subscribe します。

上で説明したようにこの Observable は USState[] 型なので、subscribe の第一パラメータでは USState[] を受けとります。

失敗したときのパス (err => のところ) では何かを受け取りますが、toString して処理してます。

上記サービスでは確かに string を throw してましたので、サービスから飛んできた例外処理なら string のはずです。

しかし、サービスからのエラーを catch する以外にも、成功側の { } の内部を処理中とかにも何らかの例外がでても、ここで catch されますので、初めから string を想定せずに、 toString してます。

検索フォームのテンプレート (src/app/search-form.component.html) は次の通りです。

<input
  [formControl]="textControl"
  type="text">
<div *ngIf="isLoading"><i class="fa fa-spin fa-spinner"></i> Loading...</div>
<div *ngIf="didSearch && !isLoading">
  <div>Result: <strong>{{search}}</strong> {{resultNum}} state(s) found. {{err}}</div>
  <ul>
    <li *ngFor="let state of states">
      {{state.StateCode}}: {{state.StateName}}, {{state.CapitalCity}}
    </li>
  </ul>
</div>

フォームはリアクティブフォームにして、input 要素に formControl を繋げています。そして valueChanges を subscribe します。この点に付いては、 「リアクティブフォームでの valueChanges の利用方法」をみてください。

ルートモジュールの構成

ルートモジュール (app/src/app.modulets) は次の通り。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import { StateSearchComponent } from './state-search.component';

@NgModule({
  declarations: [
    AppComponent,
    StateSearchComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

検索フォームを使う

上記検索フォームを使うのは、app.component.html を書き換えました。

<div>US State Search: <state-search></state-search></div>

以上で、Observable を返すサービスの作成方法及び利用方法について具体例を示しました。