この記事はangular.ioにある一連のチュートリアルの最後の章だけ切り取っているので、これだけでもAngularのHttpまわりはなんとなくわかるけどヒーローがどうとかは意味がわからないと思うので https://angular.io/tutorial から https://angular.io/tutorial/toh-pt5 まで読むか、ng-japanさんがチュートリアルをベースに作成されたハンズオンの https://github.com/ng-japan/hands-on をやるといいかもしれない。
HTTPがメインなので、コンポーネントに関するところはスキップしている。
HTTP
Providing HTTP Services
HttpModule
はAngularのコアモジュールではないので、 @angular/http
を別途インストールしてアプリモジュールにimportする必要がある。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppRoutingModule } from './app-routing.module';
...
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
AppRoutingModule
],
...
})
export class AppModule { }
すると Http
をサービスから使えるようになる。
export class MyService {
constructor(private http: Http) { }
fetchTodos() {
return this.http.get('/api/todos');
}
}
Simulate the web API
ヒーローデータのリクエストを処理できるWebサーバーがないので、in-memory web APIでサービスをモックする。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppRoutingModule } from './app-routing.module';
// Imports for loading & configuring the in-memory web api
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
...
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
InMemoryWebApiModule.forRoot(InMemoryDataService),
AppRoutingModule
],
...
})
export class AppModule { }
(json-serverあたり使ったほうが楽かつ今後も役に立ちそうな気も )
Heroes and HTTP
前章までのモックデータを使っていた箇所が以下のように書き換えられる。
getHeroes(): Promise<Hero[]> {
return Promise.resolve(HEROES);
}
import { Headers, Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';
@Injectable()
export class HeroService {
private heroesUrl = 'api/heroes'; // URL to web api
constructor(private http: Http) { }
getHeroes(): Promise<Hero[]> {
return this.http.get(this.heroesUrl)
.toPromise()
.then(response => response.json().data as Hero[])
.catch(this.handleError);
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error); // for demo purposes only
return Promise.reject(error.message || error);
}
...
}
Angularの http.get
は Observable
を返す。 Observableは非同期なデータフローを扱う強力なツールだが、それについては後述し、ここではPromiseに変換する。
Observableと .toPromise()
で変換したが、 Observable
にデフォルトで生えているメソッドではない。toPromise以外のオペレータも同様で、次のようにRxJSからインポートする必要がある。なんでそんなめんどいことになってるのかも後述する。
import 'rxjs/add/operator/toPromise';
Extracting the data in the then
callback, Error Handling
then
で成功時の、 catch
で失敗時の処理をする。
getHeroes(): Promise<Hero[]> {
return this.http.get(this.heroesUrl)
.toPromise()
.then(response => response.json().data as Hero[])
.catch(this.handleError);
}
responseの .json()
でbodyのJSONを取り出す。
private handleError(error: any): Promise<any> {
console.error('An error occurred', error); // for demo purposes only
return Promise.reject(error.message || error);
}
ここでは Promise.reject
しているので、このメソッドを呼んだ側にはエラーが返る。
(メモ)
JavaScriptの標準APIにXHRの後継のようなfetch APIがあり、 @angular/http
はそれと似たAPIになっている。 似ている ので違うところもある。
-
Response#json()
がfetchだとPromiseを返すが、AngularではPromiseじゃない -
Response
とコードに書いてもTypeScriptがlib.dom.d.ts
とかlib.es6.d.ts
に定義してあるResponse
と解釈しておかしなことになることがある-
import { Http, Response } from '@angular/http';
でOK - Responseをimportせずに書いても(lib.dom.d.tsのResponseになってしまっていても)、↓みたいにちゃんと型を明記してればTypeScriptがなんかおかしいぞと言ってくるので気付ける
-
/*
[ts]
Type 'Observable<Response>' is not assignable to type 'Observable<Response>'. Two different types with this name exist, but they are unrelated.
Type 'Response' is not assignable to type 'Response'. Two different types with this name exist, but they are unrelated.
Property 'body' is missing in type 'Response'.
*/
fetch(): Observable<Response> {
return this.http.get('api/heroes');
}
Get hero by id
api/hero/11
みたいなAPIを叩きたいときは、メソッドの引数にidを持ってそれをURLに入れてやれば良い。
getHero(id: number): Promise<Hero> {
const url = `${this.heroesUrl}/${id}`;
return this.http.get(url)
.toPromise()
.then(response => response.json().data as Hero)
.catch(this.handleError);
}
Updating hero details
PUT /api/hero/3
のようなPUTメソッドは http.put
で行う。putメソッドのシグネチャは put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response>;
private headers = new Headers({'Content-Type': 'application/json'});
update(hero: Hero): Promise<Hero> {
const url = `${this.heroesUrl}/${hero.id}`;
return this.http
.put(url, JSON.stringify(hero), {headers: this.headers})
.toPromise()
.then(() => hero)
.catch(this.handleError);
}
POST, DELETEも同様
create(name: string): Promise<Hero> {
return this.http
.post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers})
.toPromise()
.then(res => res.json().data as Hero)
.catch(this.handleError);
}
delete(id: number): Promise<void> {
const url = `${this.heroesUrl}/${id}`;
return this.http.delete(url, {headers: this.headers})
.toPromise()
.then(() => null)
.catch(this.handleError);
}
Observables
Httpのメソッドは Observable<Response>
を返す。これまではPromiseに変換してきたが、この章ではどのように、いつ、なぜ直接Observableを返すのかについて示す。
Background
Observableはmap,filterみたいな配列に似たオペレータをたくさん持った非同期を強力に扱えるライブラリで、Angularのコアでもよく使われている。
HttpのGETみたいな、一つのデータを取っておしまいみたいなケースではPromiseへの変換もよい選択と言える。
しかし、リクエストをキャンセルして別のリクエストを投げるといった操作はPromiseでは難しいが、Observableでは簡単に行える。
ヒーローの検索は以下の通り
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import { Hero } from './hero';
@Injectable()
export class HeroSearchService {
constructor(private http: Http) {}
search(term: string): Observable<Hero[]> {
return this.http
.get(`api/heroes/?name=${term}`)
.map(response => response.json().data as Hero[]);
}
}
toPromise()
していないので、戻り値の型が Observable
になっている。 map
というのがRxJSのオペレーターの一つで、 Observable<Response>
から Observable<Hero[]>
に変換している。
コンポーネントからこのメソッドを使う。
<div id="search-component">
<h4>Hero Search</h4>
<input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
<div>
<div *ngFor="let hero of heroes | async"
(click)="gotoDetail(hero)" class="search-result" >
{{hero.name}}
</div>
</div>
</div>
export class HeroSearchComponent implements OnInit {
heroes: Observable<Hero[]>;
private searchTerms = new Subject<string>();
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
ngOnInit(): void {
this.heroes = this.searchTerms
.debounceTime(300) // wait 300ms after each keystroke before considering the term
.distinctUntilChanged() // ignore if next search term is same as previous
.switchMap(term => term // switch to new observable each time the term changes
// return the http search observable
? this.heroSearchService.search(term)
// or the observable of empty heroes if there was no search term
: Observable.of<Hero[]>([])
)
.catch(error => {
// TODO: add real error handling
console.log(error);
return Observable.of<Hero[]>([]);
});
}
}
(メモ)
ngOnInit
以外の説明
- inputのkeyupイベントで、コンポーネントのsearchメソッドが実行される
- メソッドの引数にはinput.valueが渡る。
#searchBox
でテンプレートの参照を取得している。
- メソッドの引数にはinput.valueが渡る。
- searchメソッドは、inputに入力されている値で
Subject#next
メソッドを実行する- SubjectはObservableの派生クラスで、
next
メソッドが生えているのが大きな違い。next
を実行することで、Observableストリームにデータを流すことができる。この特徴のために、よく状態を表現するのに使われる。ここでは「inputに入力された文字列」の状態を表現している。
- SubjectはObservableの派生クラスで、
ngOnInit
の説明
-
this.searchTerms
にいろいろしている。つまりinputの文字列が変化するとここに書いてある処理がイベントドリブンに動く。 - 色々した結果が
heroes: Observable<Hero[]>
に入れている。このheroes
をテンプレートで<div *ngFor="let hero of heroes | async">
しているので、まとめると、inputに文字列を入れるとなんやかんやあって結果のリストが表示されるっぽいことがわかる。 -
debounceTime(300)
300 msは処理を待つ。inputに文字が一文字入るごとにバックエンドにリクエストを投げるとエライことになるので、searchTerms
ストリームにイベントが最後に流れてから300ms何もなかったら、後続に値を流す。 -
distinctUntilChanged()
前に流れたイベントと同じなら、後続に流さないようにする。同じ検索単語のリクエストを投げても結果は同じなので、ここで省いている。 -
switchMap
新しいObservableにスイッチする。ここまでは検索単語のストリームだったのを、検索結果のストリームにスイッチしている。検索のリクエストを投げる前に、文字列が空であれば空の結果をその場で生成している。switchMap
,concatMap
,mergeMap
とか似たオペレータが多いので使い分けが難しい。switchMap
は最新の検索結果のみを後続に流す。古い検索結果のObservableはキャンセルし、破棄する。(キャンセルはそういう実装をしないとキャンセルにならず、この例だと単に古い結果を破棄するだけらしい) -
catch
Observableのエラーハンドリングをする。このメソッドのreturnでObservable.of([])
としているので、もしエラーが起こった際は空のリストが結果として返るようになっている。throw new Error('...')
とすれば後続にエラーが流れる。
(ドキュメントにも似たような説明あるの気づかずに書いてしまったので気になる人はドキュメントも読んで )
Import RxJS operators
Observableに生えてるメソッドやObservableの派生クラスを使うときは別途importする。パターンとして大体3つに分けられる。
// Observableやその派生クラス
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
// Observableの静的メソッド
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/create';
// Observableのオペレーター
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
Home Stretch
この章でやったこと
- HTTPモジュールからインポートした
- web APIを使ってHeroServiceをリファクタした
- get(), post(), put(), delete()メソッドを使った
- コンポーネントからヒーローを追加、編集、削除できるようにした(この記事では全部スキップした
)
- in-memory-web-apiを使った
- Observableがチョットワカッタ