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の仕組みを組み合わせて、通信の非同期処理を簡潔に記述できます。
