Help us understand the problem. What is going on with this article?

Angular HTTP Tutorial メモ

More than 1 year has passed since last update.

https://angular.io/tutorial/toh-pt6

この記事は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でサービスをモックする。

https://github.com/angular/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あたり使ったほうが楽かつ今後も役に立ちそうな気も :innocent: )

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.getObservable を返す。 Observableは非同期なデータフローを扱う強力なツールだが、それについては後述し、ここではPromiseに変換する。 :innocent:

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 と解釈しておかしなことになることがある :innocent:
    • 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[]> に変換している。
コンポーネントからこのメソッドを使う。

hero-search.component.html
<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>
hero-search.component.ts
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 でテンプレートの参照を取得している。
  • searchメソッドは、inputに入力されている値で Subject#next メソッドを実行する
    • SubjectはObservableの派生クラスで、 next メソッドが生えているのが大きな違い。 next を実行することで、Observableストリームにデータを流すことができる。この特徴のために、よく状態を表現するのに使われる。ここでは「inputに入力された文字列」の状態を表現している。

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('...') とすれば後続にエラーが流れる。

(ドキュメントにも似たような説明あるの気づかずに書いてしまったので気になる人はドキュメントも読んで :innocent: )

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()メソッドを使った
  • コンポーネントからヒーローを追加、編集、削除できるようにした(この記事では全部スキップした :innocent: )
  • in-memory-web-apiを使った
  • Observableがチョットワカッタ

https://angular.io/tutorial/toh-pt6

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away