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 を返すサービスの作成方法及び利用方法について具体例を示しました。