Angular2のHttpモジュールを眺めてベストプラクティスを考える


@laco0416 です。今回はHttpモジュールの機能を眺め、アプリケーション中で使う際のベストプラクティス案みたいなものを考えていこうと思います。
なお、本稿で扱うAngular2のバージョンはbeta.0です

基本的な使い方

まずはざっとAPIリファレンスを眺めます。
https://angular.io/docs/ts/latest/api/

重要なのは以下のシンボルです。

  • HTTP_PROVIDERS
  • Http
  • Request
  • Response

HTTP_PROVIDERSはHttpクラスのインスタンスをDIで取得するために必要なプロバイダのセットです。テストでモックサーバーなどを差し込みたい場合はこのプロバイダをいじくります。

HttpはAjaxの主役、いわゆるクライアントです。このクラスのメソッドでリクエストを送り、レスポンスを受け取ります。

RequestはHttpクラスがリクエストを送る際に使うクラスです。Fetchの仕様を意識した(一致はしてない)インタフェースになっています。

Responseはレスポンスを受け取るときに使うクラスです。Httpクラスでリクエストを送るメソッドの戻り値はすべて Observable<Response> という型になっています。これもFetchの仕様を意識した(微妙に異なる)インタフェースになっています。

Httpモジュールを使う簡単な例がこちら。適当にファイルにアクセスするだけです。

import {bootstrap} from 'angular2/platform/browser';
import {Component} from 'angular2/core';
import {HTTP_PROVIDERS, Http, Request, Response} from 'angular2/http';

@Component({
  selector: "my-app",
  template: `
  <h3>Http example</h3>
  <button (click)="updateStatus();">Get data.json</button>
  <p>Status: {{ status }}</p>
  <pre>{{ body | json }}</pre>
  `
})
class MyApp {

  private status: number;
  private body: string;  

  constructor(private http: Http) {
  }

  updateStatus() {
    this.http.request(new Request({
      method: "Get",
      url: "./data.json"
    })).subscribe((res: Response) => {
      this.status = res.status;
      this.body = res.json();
    });
  }
}

bootstrap(MyApp, [HTTP_PROVIDERS]);

Plunker: http://plnkr.co/edit/u51QMqOjfMmfgMxzDsC9?p=preview

ポイントは

  • bootstrap()の第2引数でHTTP_PROVIDERSを渡す
  • subscribeでレスポンスに対する反応を記述する
  • レスポンスのJSONを res.json()で取得する

あたりでしょうか。ちなみにJSONではなくtext/plainで受け取りたいときはres.text()を使います。
また、http.requestを使っていますがGetリクエストの場合は次のようにhttp.getで簡略化できます

this.http.get("./data.json").subscribe(...);

サービスとして分離する

ただ使うだけならComponentで直接HttpをDIしていいですが、AngularJS時代からAjaxを行うサービスを分離してDIするのが主流ですね。Angular2でも同じことはもちろん可能です。
以下の例では@Injectableを付けてDI可能にしたSampleServiceクラスでHttpをDIしています。また、Observable.map関数を使って、Observable<Response>型からObservable<Sample>型に変換し、コンポーネント側から使いやすいようにしています。

import {bootstrap} from 'angular2/platform/browser';
import {Component, Injectable} from 'angular2/core';
import {HTTP_PROVIDERS, Http, Request, Response} from 'angular2/http';
import 'rxjs/add/operator/map';

// データの型
interface Sample {
}

// Ajaxを行うサービス
@Injectable()
class SampleService {

  constructor(private http: Http) {
  }

  fetch(): Observable<Sample> {
    return this.http.get("./data.json").map(res=>res.json() as Sample);
  }
}

// サービスを呼び出すコンポーネント
@Component({
  selector: 'my-app',
  template: `
    <h3>Http example</h3>
    <pre>{{ data | json }}</pre>
  `,
  providers: [SampleService]
})
export class MyApp {
  public data: Sample;

  constructor(private service: SampleService) {
  }

  ngOnInit() {
    this.service.fetch().subscribe(data=>this.data = data);
  }
}

bootstrap(MyApp, [HTTP_PROVIDERS]);

Plunker: http://plnkr.co/edit/GcDbZY5CsKQItsbjdxTi?p=info

サービスを実装した分コードは増えましたが見通しはよくなりました。サービスとして分離したのでどのコンポーネントからでもDIで呼び出しできるようになっています。

Observable.map関数を使うにはRxJSを読み込む必要があります。Plunker環境ではAngular2用にバンドルされたCDN配布版を使っているので、map関数を個別にインポートしています。

import 'rxjs/add/operator/map';

もしWebpack等のCommonJS系ツールを使っているなら次の1行をアプリケーションのエントリポイントで記述しておきましょう。

import 'rxjs/Rx';

Promise v.s. Observable

先ほどの例ではサービスからの戻り値の型をObservable<Sample>としました(長いので以下O<T>とする)。Httpクラスのメソッドの戻り値もO<Response>となっていますが、なぜPromiseではないのでしょうか?
Promiseは1回の非同期処理の結果を扱うための概念です。AngularJSでもサポートされ、今では一般的なAPIの一つになりつつありますが、いくつか欠点もあります。

  • 連続した非同期処理の結果を扱えない
  • 遅延実行できない。即実行され、resolveした直後にthenが実行される
  • キャンセルできない。
  • 一度resolve/rejectした後に状態を変えられない

一方ObservableはStreamであり、結果の個数・回数は無制限です。また、subscribeされるまでは次の処理は実行されず、さらにunsubscribeすることでキャンセルすることもできます。

ところで、モダンなWebアプリケーションで非同期処理が必要な場面は大きくわけると次の6つになります。

  • DOM Event
  • Animations
  • Ajax
  • WebSocket
  • Server-Sent Event
  • その他Input(voice等)

このうちPromiseで解決可能なものはAjaxだけです。他は繰り返し非同期処理が行われ、時間とともに値が変わるものばかりです。Ajaxにしても、Promiseではキャンセルできず、レスポンスを待っている間にページが切り替わっても中断できません。これらの点から、Angular2では非同期処理の根幹をObservableに挿げ替えました。そして我々のアプリケーションコード中でもできる限りObservableをベースに非同期処理を扱うべきです。先ほどのサービスクラスではHttpクラスのメソッドから生成したObservableをmapによって変換していますが、先述の通りsubscribeするまでは実行されないので、コンポーネントに渡るまでは実行が遅延されています。

ここらへんの話はAngularConnectのRxJS in depthを見ると良いです。

RxJSの機能を活用する

HttpモジュールとRxJSの機能を活用することでPromiseベースでは複雑になってしまうコードが簡潔に記述できるようになります。

エラーの際にn回リトライする

1回目のリクエストに失敗した際にすぐにエラーとするのではなく2,3回リトライするようなコードは1回しか結果を返せないPromiseでは難しい処理ですが、RxJSを使うと次のコードで済みます。

this.http.get("data/data.json").retry(5).map(res => res.json() as Sample);

retry(n)はレスポンスのステータスコードが200-299の間に無かった場合に自動でリトライする回数を設定できる関数です。

毎秒リクエストを送る

一定時間おきにリクエストを送るようなコードも次のように簡単に書けます

    return Observable.interval(1000)
      .flatMap(() => this.http.get("./data.json"))
      .map(res=>res.json() as Sample);

完了時に処理を行う

Observableが完了した時に処理を行うのはfinallyを使います

    return this.http.get("data/data.json")
      .map(res => res.json() as Sample)
      .finally(()=>{
        console.log("end")
      });

Promiseに変換する

どうしても他のライブラリとの連携でPromiseが必要な場合はObservableをPromiseに変換できます。toPromiseメソッドにはPromiseのコンストラクタを渡します。

    this.service.fetch().toPromise(Promise).then(data => {
      this.data = data;
    });

まとめ

後半はRxJSの紹介のようになってしまいましたが、Angular2のHttpモジュールを使う上でのオススメ構成は以下のとおりです。

  • Httpクラスを使うのはサービスクラスに切り出して@InjectableでDIする
  • レスポンスはObservableで扱う

まだコアの部分も不安定で、そのプラグインであるHttpモジュールのベストプラクティスなんてまだどうしても定まるものではないですが、ざっくりとした設計方針は自分の中で決まりつつあります。せっかく連携できるようになっているので、ObservableとRxJSを活用してリアクティブなアプリケーションをデザインしたいものです。alphaなうちに試行錯誤してbetaが出る頃にはバリバリAngular2アプリケーション書けるようになりましょう!
betaが出たのでバリバリ書きましょう!!

おまけ

beta.0のおまじない

  <script src="lib/angular2-polyfills.js"></script>

webpackの場合は angular2-seedを見れば大丈夫なはずです。

宣伝

12/31のコミックマーケット89で頒布されるTechBoosterの新刊「JavaScriptoon2」でAngular2のことを書いてます。使い方というよりはAngular2の思想、根底にあるものに重きをおいた内容です。興味があればぜひお買い求めください!
https://techbooster.github.io/c89/#scriptoon2
コミケに抵抗がある方はBoothにて通販も行いますのでご心配なく!