次の方法で共有



October 2017

Volume 32 Number 10

働くプログラマ - MEAN あれこれ: Angular によるフェッチ

Ted Neward | October 2017

Ted Neward「MEANers」の皆さん、お帰りなさい。

前回のコラムで、「Angular アプリケーションに求められる一般的な機能の 1 つは、ブラウザー以外の場所、通常はあらゆる場所からのデータの取得と更新である」と説明しました。 そして、HTTP を使って実際に何かを行うこともなく、説明だけでコラムを終えました。

釈明すると、前回のコラムの目的は、HTTP 要求コードを Angular サービスの内部に配置することで HTTP API からデータをフェッチできるようにコードを準備することでした。これを目的にする前は Angular サービスに注目していなかったため、最初に Angular サービスがどのようなもので、Angular サービスが必要になる可能性のあるさまざまなコンポーネントでこのサービスをどのように使用するかを説明しておく必要がありました。ところが、このような基礎部分を説明しているうちに、本来の目的であった HTTP API エンドポイントに存在するデータについて取り上げるスペースがなくなってしまいました。

今回は、この問題を解決します。データをフェッチしましょう。

Speaker API

まず、要求にアクセスする HTTP API が必要です。MSDN マガジンの大量のバックナンバーの中に、筆者が 1 年前にビルドしたサーバー側 API の最新のバージョンがありました。当時のコラムを詳しく調べると、それが speaker ではなく「Person」を提供する API であることがわかります。プロジェクトの目的が時を経てこれほど変化するとは驚きです。

MEAN サーバーが非常に単純で、当時と現在の違いは返されるオブジェクトの種類だけであることを考えると、(先月のコラムで定義した) Speaker オブジェクトを受け渡しするようにコードを簡単に変更できます。しかし、コードを変更すると、別の絶好の機会を逃すことになります。Node.js コミュニティには、大量の「MEAN スタック フレームワーク」があり、Sails.js (sailsjs.com)、筆者が扱っている Loopback (strongloop.com) もあります。こうしたフレームワークの多くは、サーバー側 HTTP API を構築するために、筆者が 1 年前に使用したのと同じツール (Express、MongoDB など) を使用して組み立てられていますが、すべてが密接に結び付き、内部がわからないように隠ぺいされています。また、ASP.NET MVC Web API エンドポイントにまとめることもできます。サーバー側の実装は、HTTP に隠れている限り問題になりません。

まず、API のインターフェースがどのようなものか確認します。具体的には、使用する URL、それらの URL に使用する動詞、想定するパラメーター、返される JSON 応答の形式などを確認します。Angular が非常に簡単なサービスであることを考えると、その内容を想像するのは簡単です。そのため、ここですべてを一覧する必要はありません (speaker の完全なリストを得るには、「GET on /speakers」と入力し、特定の speaker を取得するには、「GET on /speakers/:id」と入力するだけです)。

SpeakerService

前回説明したように、データをフェッチするモジュールを SpeakerService としました。サービス全体のポイントは、Speaker のフェッチと格納の詳細をカプセル化することなので、今回説明する予定の内容はほぼすべてここに含めます。

前回中断したときに、SpeakerService はインメモリ定数から結果を描画しました。

@Injectable()
export class SpeakerService {
  constructor() { }
  getSpeakers() : Array<Speaker> {
    return SPEAKERS;
  }
  getSpeaker(id: number) : Speaker {
    return SPEAKERS.find(speaker => speaker.id === id);
  }
}

最終的には、サービスで更新と削除の機能を提供する必要がありますが、ここでは読み取り操作を見ていきます。各ケースで、"get" メソッドが immediate オブジェクトをどのように返すか、つまり Speaker 自体を返すのか Speaker の配列を返すかがわかります。これは、インメモリ操作では合理的ですが、TCP/IP を使って行う操作では合理的とはいえません。ネットワーク操作には時間がかかるため、ネットワーク操作が終了するまで (決して終了しないと仮定し) 呼び出し元に待機するように強制すれば、フロントエンドを構築する開発者に過度の負担をかけることになります。フロントエンドでは、応答を待機するためにブロックしながら UI の応答性を維持しなければならなくなるためです。従来、他の UI プラットフォームでは、UI に複数のスレッドを使用し、開発者にこうした矛盾の責任を負わせるという解決策をとってきました。ただし、Node.js では、実際のオブジェクトではなく、約束事を返す方法が好まれます。この約束事によって最終的に実際のオブジェクトが示されます。この約束事は、ご想像の通り、プロミスと呼ばれます。

プロミスを簡単に説明すれば、将来のある時点で望ましい結果を保持するプレースホルダーで、1 つのオブジェクトです。しかしその時点までは、実際の結果がもたらされることなく実行を継続します。基本的に、プロミスは、必要な値を生成する、「resolver」と呼ばれるコールバックにラップされるオブジェクトです (実際には、プロミスは 2 つのコールバックにラップされます。resolver 以外のもう 1 つのコールバックは、プロミスが役割を果たせない場合に備えるもので「rejecter」と呼ばれます)。

HTTP トラフィックは本質的に速度が遅いため、少なくともネットワーク トラフィックに配置するまでは、サービスをこのようにモデル化する必要があります。プロミスへの切り替えとデータを返す際の 2 秒間の遅延を組み合わせれば十分です。図 1 に変更したコードを示します。

図 1 Promise プレースホルダーを含むサービスのモデル化

@Injectable()
export class SpeakerService {
  constructor() { }
  getSpeakers(): Promise<Array<Speaker>> {
    return new Promise((resolve, reject) => {
      setTimeout( () => {
        resolve(SPEAKERS);
      }, 2000);
    });
  }
  getSpeaker(id: number): Promise<Speaker> {
    return new Promise((resolve, reject) => {
      setTimeout( () => {
      resolve(SPEAKERS.find(speaker => speaker.id === id));
      }, 2000);
    });
  }
}

このコードは、データを提供する前に、2 秒間のネットワークの遅延をシミュレーションします。しかし、ネットワーク状態をより入念にシュミレーションする場合は、データの中にランダムなエラーを混在させ、約 20 回の呼び出しごとに reject の呼び出しが行われるようにします。

Upvotes など

コンポーネント側では、実際のデータではなく、プロミスを受け取るために、テンプレートとロジックを多少調整しなければなりません。これは図 2 に示すように、実際に行ってみると簡単です。

図 2 Speaker 値の使用

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'SpeakerApp';
  speaker: Speaker = new Speaker();

  constructor(private speakerSvc: SpeakerService) {
    this.speakerSvc.getSpeaker(1).then( (data) => {
      this.speaker = data;
    });
  }
}

大きな変更を 2 つ加えています。まず、speaker フィールドを空の Speaker 値に初期化しています。これにより、Angular が最初にコンポーネントのテンプレートを初期化する際に使用できる Speaker オブジェクトが常に存在するようになります。次に、返されたプロミスを使用するために、then メソッドの呼び出しを用意するようにコンストラクターを修正します。これにより、プロミスが処理するデータを取得し、使用します。今回は、コンストラクターをコンポーネントの speaker フィールドに保存します。テンプレートは変更しません。そのため、「ng serve」がバックグランドで依然として実行されている場合、ページを再度読み込むと、興味深い事態が生じます。ページは表示されますが、Upvote コンポーネントの "votes" 部分は 2 秒間空のままです。その後、まるで魔法のように、フェッチされた値が表示されます。

これがプロミスのメリットです。Angular は、値が変化したときに、変更された DOM の部分を再レンダリングする必要があることを認識しています。そして、これが自動的に行われます。すばらしい。

Speaker はどこにある

しかし、依然としてデータをローカルにフェッチしています。それでは意味がありません。SpeakerService に話を戻し、HTTP を経由した呼び出しを開始します。つまり、いくつか変更を加えます。HttpModule をインポートし (Angular チームは HttpModule を「コア」から外しました)、HttpService を参照して SpeakerService で使用します。(ご想像のとおり)、HttpService はプロミスを使用するため、SpeakerService API における微妙な変化により HttpService の使用が図 3 に示すように非常に簡単になります。

図 3 HttpService の使用

@Injectable()
export class SpeakerService {
  private speakersUrl = "http://localhost:3000/api/Speakers";

  constructor(private http : Http) { }
  getSpeaker(id: number): Promise<Speaker> {
    return this.http.get(`${this.speakersUrl}/${id}`)
               .toPromise()
               .then(response => {
                  let raw: any = response.json();
                  let speaker = new Speaker();
                  speaker.firstName = raw.FirstName;
                  speaker.lastName = raw.LastName;
                  speaker.votes = new Upvote(raw.Votes);
                  speaker.id = raw.id;
                  return speaker;
               });
  }

toPromise を呼び出す理由は、Observable について今後説明していくと明確になります。ここでは、返されたオブジェクトをプロミスとして使用できるように変換するために必要です。図 3 のコードを動作させるには、さらにもう 1 つのインポートが必要であることに注意してください。toPromise を取り入れるには、メソッドをファイルの先頭に以下のコードを追加します。

import 'rxjs/add/operator/toPromise';

SpeakerService が API から返された JSON をローカルの Speaker オブジェクトに変換するのを不思議に思われるかもしれません。しかし、これは実際のところかなり一般的な作業です。API によって公開される JSON は、UI で使用するには適切ではないことがよくあります。そのため、ローカルの概念でその型のあるべき形式に変換する必要があります。今回は、サービスが speaker を大文字のフィールド名で返す (これは、皆さんが思っているよりも珍しくはありません) と仮定し、"Votes" フィールドを Upvote オブジェクトに変換するために追加の手段を講じています。一部の「フル スタック」システムは、クライアントとサーバーの両方で共有可能な単一のスキーマ定義があるとしています。しかし、一般的にはそのようなスキーマがないと仮定して調整を行い、受け取ったデータを UI に受け入れられやすいように変換する方が安全です。

場合によっては、調整とは、一部のデータの除外、使いやすくするためのデータのフラット化、別の変換などを意味します。たとえば、サーバーが 2 つの異なる "Speaker" と "Talk" オブジェクトを返したら、SpeakerService はこれらのオブジェクトを、筆者が使用している単一の Speaker オブジェクトにフラット化し、speaker リポジトリの各通信へのすべての vote を単一のスカラー値に収集します。

その後保存します。ポート番号が 3000 の localhost でサーバーを実行していると仮定すると、HTTP の使用に切り替える前と同様にすべてが動作します (localhost で実行されるサーバーがない場合は、SpeakerService の URL を http://msdn-mean.azurewebsites.net/ に変更してください。筆者は、最近修正した Speaker API をこのサイトに配置しています。"/explorer" URL を使用して、API エンドポイントの完全なセットを必要に応じて参照してください。このセットは、Swagger で生成された UI で、Loopback の操作時に得られたものです)。

まとめ

HttpModule (およびそれに対応するサービス オブジェクトである HTTP) は確かに PUT と POST をサポートするだけでなく、HTTP がもたらすメリットをサポートします。しかし、Angular アプリに新しい Speaker を編集および追加する機能を追加すれば、これらに対処できるようになります。この点については、すべての speaker の一覧を表示し、ユーザーが特定の speaker をドリルダウンできるようにすることでもう少しこのフロントエンドを調整する必要があります。このアプローチは、非常に多くのアプリケーションで使用 (乱用) されています。そのためには、(コンポーネントがアプリケーション内の「ページ」であるかのようにコンポーネント間を移動できるようにするための) 「ルーティング」と、(データ入力を行うための)「フォーム」を Angular で管理する方法を説明する必要があります。これについては、次回取り上げます。そのときまで、コーディングを楽しんでください。


Ted Newardは、シアトルを拠点に活躍している、ポリテクノロジーに関するコンサルタント、講演者、および指導者です。現在は、Smartsheet.com で、開発者リレーションのディレクターを務めています。これまでに非常に多くの記事を執筆している Ted は、さまざまな書籍を執筆および共同執筆し、世界を股に掛けて仕事をしています。彼の連絡先は、ted@tedneward.com です。また、blogs.tedneward.com にブログを公開しています。

この記事のレビューに協力してくれた技術スタッフの James Bender に心より感謝いたします。


この記事について MSDN マガジン フォーラムで議論する