HTTP通信処理をサービスに分離
ここまでは、HTTP通信処理をコンポーネントに直接実装しましたが、コードのメンテナンス性や可読性を考えると、通信処理をコンポーネントから分離しておくのが望ましいです。以下では、リスト3の実装をもとに、通信処理をサービスに切り出して、依存性注入でコンポーネントに提供する方法を説明します。サービスと依存性注入については過去記事も参考にしてください。
まず、リスト4のようなサービスクラスを作ります。
import { Injectable } from "@angular/core"; import { Http, Response, Headers, RequestOptions } from "@angular/http"; import { Observable } from "rxjs/Observable"; // RxJSのメソッド ...(1) import "rxjs/add/observable/throw"; import "rxjs/add/operator/map"; import "rxjs/add/operator/catch"; @Injectable() export class HttpExec { // コンストラクター ...(2) constructor(private http:Http) { } // URLとPOSTデータを引数にHTTP POSTを実行 doJsonPost(url:string, postData:Object):Observable<string> { let headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded" }); let options = new RequestOptions({ headers: headers }); // postメソッドでObservableを返す ...(3) return this.http.post(url, postData, options) .map(this.extractData) .catch(this.handleError); } // APIレスポンスからレスポンス文字列を取得 ...(4) private extractData(res: Response) { // 戻り値を文字列で取得 let body = res.text(); // nullの場合は空文字を返却 return body || "" } // エラーハンドル処理 ...(5) private handleError (error: Response | any) { let errMsg: string; // Responseオブジェクトからエラーを取得してエラー文言を作成 if (error instanceof Response) { errMsg = error.status + ":" + error.statusText; } // errorがResponseオブジェクトでない場合は、可能な限りエラー文言を取得 else { errMsg = error.message ? error.message : error.toString(); } // エラー文言を戻す return Observable.throw(errMsg); } }
(1)で、JavaScriptのRxライブラリ(RxJS)からthrow、map、catchメソッドを参照しています(利用法は後述します)。サービスクラスのコンストラクター(2)でHttpクラスのインスタンスを受け取り、POST通信を実行するdoJsonPostメソッド内で利用します(3)。
(3)で実行されているmapメソッドは、Observableから取得した値を変換するメソッドで、ここではResponseオブジェクトからレスポンス文字列を取得する処理(4)を指定します。また、エラー発生時の処理を指定するcatchメソッドでは、エラー文言をObservable.throwメソッドで戻す処理(5)を指定しています。(4)と(5)では、nullのレスポンス文字列や、Responseクラス以外のエラーオブジェクトに対応する異常系の実装を行っています。
リスト4のサービスを使うコンポーネントの実装はリスト5になります。
// コンストラクター ...(1) constructor(private httpExec:HttpExec) { } // ボタン押下時の処理 private onClickPost() { // POSTデータ let postData = "title=" + encodeURIComponent("POSTテストデータ") + "&value=" + encodeURIComponent("HTTP POSTで送信したデータです。"); // doJsonPostメソッドの戻り値をsubscribeして、内容を取得 ...(2) this.httpExec.doJsonPost("http://localhost:3030/post-test-data", postData) .subscribe( // 成功時処理 ...(3) resString => { this.resString = resString; }, // エラー時処理 ...(4) errMsg => { console.error(errMsg); } ); }
リスト4で実装したHttpExecサービスのインスタンスを(1)のコンストラクターで受け取り、(2)でサービスのdoJsonPostメソッドを実行します。戻り値のObservableオブジェクトから、subscribeメソッドで結果を受け取ります。成功時処理(3)の引数resStringにはリスト4(4)で作成したレスポンス文字列が、エラー時処理(4)の引数errMsgにはリスト4(5)で作成したエラー文言が渡されます。
リスト4、5のようにHTTP通信処理を切り出しても、切り出す前(図2)とまったく同じく動作します。
このように、HTTP通信処理固有の実装をサービスに切り出して、コンポーネントから分離しておけば、例えばサービスの実装を別のものに置き換えてもコンポーネント側に影響を及ぼさないようにできます。
[参考]ObservableとPromise
ここまで説明したように、Angular 2のHTTPクライアントは、非同期処理を実現するRxとObservableに依存しています。一方で、Promiseと呼ばれる非同期処理の実装方法が、ECMAScript 2015から利用可能になりました。
Angular 2のHTTPクライアントが返却するObservableオブジェクトは、toPromiseメソッドでPromiseオブジェクトに変換できます。リスト4、5をもとに、ObservableをPromiseに変換する例(angular-004-promise)を、ダウンロードできるサンプルに含めています。
JSONPでWebAPIにアクセス
Angular 2のHttpクラスによるHTTP通信は、いわゆる「同一オリジンポリシー」により、外部Webサーバーへのアクセスが制限されます。そのため、さまざまなWebページからのアクセスが想定されるWebAPIでは、この制限を回避できるJSONP(JSON with padding)が利用されます。
以下では、Angular 2のHTTPクライアントでJSONPを利用する例として、Yahoo!ショッピングの商品検索APIで、商品をキーワード検索するサンプルを紹介します。このサンプルでは、指定したキーワードにマッチした製品の写真・名称・価格がリスト表示されます。
JSONPの機能を提供するAngular 2のモジュールはJsonpModuleです。リスト1でHttpModuleを設定したのと同じように、ルートモジュールにJsonpModuleを設定します。
WebAPIを実行するサービスの実装はリスト6のようになります。
@Injectable() export class YahooAPI { // アプリケーションID ...(1) APP_ID = "<アプリケーションIDを設定>"; // APIのURL ...(2) API_URL = "http://shopping.yahooapis.jp/ShoppingWebService/V1/json/itemSearch"; // コンストラクター ...(3) constructor(private jsonp:Jsonp) { } // Yahoo!ショッピングの商品検索APIを実行 searchShopping(query:string):Observable<Object> { // 検索パラメーターを設定 ...(4) let params = new URLSearchParams(); params.set("appid", this.APP_ID); // アプリケーションID params.set("query", query); // 検索文字列 params.set('callback', 'JSONP_CALLBACK'); // JSONPのコールバック名 // 検索パラメーターをリクエストオプションに設定 ...(5) let options = new RequestOptions({search: params}); // jsonp.getメソッドでAPIを実行するObservableを返す ...(6) return this.jsonp.get(this.API_URL, options) .map(this.extractData) .catch(this.handleError); } // APIレスポンスからJSONオブジェクトを取得 ...(7) private extractData(res: Response) { // 戻り値をJSONオブジェクトして取得して返却 let body = res.json(); // nullの場合は空オブジェクトを返却 return body || { }; } (handleErrorは略) }
(1)にはYahoo!の開発者サイトで取得できるアプリケーションIDを設定します。(2)はAPIのURLです。(3)のコンストラクターで、JSONPの処理を実行するJsonpクラスのインスタンスを受け取ります。
APIを実行するには、まずWebAPIに与える検索パラメーターをURLSearchParamsオブジェクトに設定します(4)。ここでは、アプリケーションIDと検索文字列、JSONPのコールバック名を指定します。APIに指定するパラメーターはYahoo!のドキュメントも参照してください。(5)では、リクエストオプションを表すRequestOptionsオブジェクトを生成して、searchプロパティに(4)のURLSearchParamsオブジェクトを設定しています。
JSONPでAPIを実行する処理は(6)です。JsonpクラスはHttpクラスのサブクラスなので、Httpクラスと同一形式で記述できます。JSONP固有の処理は内部で行われるため、実装時にJSONPであることを意識する必要はありません。
APIの戻り値をJSONオブジェクトで引き渡すため、(7)のextractDataメソッドで、ResponseオブジェクトのjsonメソッドでJSONオブジェクトを取得して返却します。エラーハンドル処理handleErrorの処理はリスト4と同様です。
コンポーネント側では依存性注入でYahooAPIクラスのインスタンスを受け取って、searchShoppingメソッドで検索を実行します。詳細はサンプルコードを参照してください。
まとめ
本記事では、Angular 2が提供するHTTPクライアント機能を利用して、Webサーバーと非同期通信を行う方法を説明しました。Angular 2のHttpModule/JsonpModuleに含まれるHttpクラスやJsonpクラスと、Reactive ExtensionsのObservableやPromiseの仕組みを組み合わせて、通信の非同期処理を簡潔に記述できます。