06.非同期処理
概要
ここでは、操作性が高く使い勝手の良いリッチな画面を実現するために必須となる、非同期処理の取り扱いについて説明します。
非同期でサーバとのデータのやりとりができれば、画面遷移を減らすことができ、サーバとの通信中もブラウザをブロックしなくて済みます。非同期処理は、使い勝手の良いリッチなウェブアプリケーションの作成には必須となります。
この章では、商品リストを非同期で取得して表示するサンプルの作成を通して、非同期処理について説明します。
実装例
サーバからデータを取得する部分はロジック、画面操作はコントローラに記述します。
以下は、画面で選択されたカテゴリと一致する商品情報をサーバから非同期で取得するロジックです。
/**
* ロジック名
*/
__name: 'ItemSearchLogic',
/**
* 商品リスト(商品名と税込価格)を取得する。
*
* @param categoryId {Number} カテゴリID
* @returns 商品リスト
*/
getItemList: function(categoryId) {
var dfd = this.deferred();
var result = null;
this._getItemData(categoryId).done(function(data) {
result = $.map(data, function(obj) {
obj.price = Math.floor(obj.price * 1.05);
return obj;
});
dfd.resolve(result);
}).fail(function(error) {
dfd.reject(error.message);
});
return dfd.promise();
},
/**
* カテゴリIDから商品(商品名と税抜価格)リストをサーバから取得する。
*
* @param categoryId {Number} カテゴリID
* @returns 商品リスト
*/
_getItemData: function(categoryId) {
// [ {"itemname":"hoge", "price": "1000"}, ...] のようなJSONオブジェクトを返す
var promise = h5.ajax('./itemList', {
type: 'GET',
dataType: 'json',
data: {
categoryId: categoryId
}
});
return promise;
}
};
- getItemListメソッドはパブリックメソッドです。コントローラから呼ばれることを想定しています。
このように、データを取得するメソッドと取得したデータに対して処理を行うメソッドと役割を分けて作成することで、getItemListとは異なる処理を行いたい場合は、データを取得するメソッド(_getItemData)はそのままで処理を行うメソッドのみを新規に作成すればよいだけなので、修正が容易です。 - getItemListでDeferredオブジェクトを作成しています。ロジックのステップにも記述しましたが、ロジック化されると、フレームワークによってdeferredメソッドが追加されます。
- getItemListでPromiseオブジェクトを返しています。
- _getItemData内のh5.ajaxは、jQueryで用意されているAjax通信を行うためのメソッドです。ajaxメソッドの戻り値はPromiseオブジェクトなので、this._getItemData(categoryId).done()のようにして、doneメソッドで非同期処理の結果を取得することができます。なお、エラーはfailメソッドで取得することができます。
次に、コントローラの記述です。
/**
* コントローラ名
*/
__name: 'ItemSearchController',
/**
* テンプレート
*/
__templates: 'template.ejs',
/**
* 商品検索ロジック
*/
itemSearchLogic: itemSearchLogic,
/**
* 検索ボタン押下アクション
*/
'#searchBtn click': function(context) {
var $resultDiv = this.$find('#resultList');
var that = this;
$resultDiv.empty();
// 画面で選択されたカテゴリ
var category = this.$find('#select-category option:selected').val();
this.itemSearchLogic.getItemList(category).done(function(resultData) {
that.view.append($resultDiv, 'template1', {listData: resultData});
}).fail(function(errMsg) {
alert('取得に失敗しました' + errMsg);
});
}
};
h5.core.controller('#container', itemSearchController);
- itemSearchLogic#getItemListを実行します。戻り値はPromiseオブジェクトです。
- getItemListの戻り値はPromiseオブジェクトです。doneコールバックにロジックの非同期処理後のコントローラの処理を記述しています。
ここでは、Ajaxで取得したデータを、一覧としてテンプレートを使用して画面に出力しています。
動作確認
jQuery Deferred APIについて
hifiveの非同期制御で用いるDeferredはjQueryのDeferred APIをベースにしています。
jQueryのDeferred APIで提供されている機能は基本的にすべて利用可能です。
deferred.notify, deferred.progressの実装例
7-1の実装例にnotify, progressを使ったコードを追記します。
/**
* ロジック名
*/
__name: 'ItemSearchLogic',
/**
* 商品リスト(商品名と税込価格)を取得する。
*
* @param categoryId {Number} カテゴリID
* @returns 商品リスト
*/
getItemList: function(categoryId) {
var dfd = this.deferred();
var result = null;
this._getItemData(categoryId).done(function(data) {
result = $.map(data, function(obj) {
dfd.notify(data.length);
obj.price = Math.floor(obj.price * 1.05);
return obj;
});
dfd.resolve(result);
}).fail(function(error) {
dfd.reject(error.message);
});
return dfd.promise();
},
/**
* カテゴリIDから商品(商品名と税抜価格)リストをサーバから取得する。
*
* @param categoryId {Number} カテゴリID
* @returns 商品リスト
*/
_getItemData: function(categoryId) {
// [ {"itemname":"hoge", "price": "1000"}, ...] のようなJSONオブジェクトを返す
var promise = h5.ajax('./itemList', {
type: 'GET',
dataType: 'json',
data: {
categoryId: categoryId
}
});
return promise;
}
};
- _getItemData()のdoneコールバックの中でdeferred.notifyを使用しています。データ1件に対する税込金額の計算が完了した件数を画面に通知しています。
今回の例では、簡単な処理のため処理がすぐに終了してしまいますが、時間のかかる処理を実行中に、途中経過をユーザに通知したりする場合に役立ちます。
/**
* コントローラ名
*/
__name: 'ItemSearchController',
/**
* テンプレート
*/
__templates: 'template.ejs',
/**
* 商品検索ロジック
*/
itemSearchLogic: itemSearchLogic,
/**
* 検索ボタン押下アクション
*/
'#searchBtn click': function(context) {
var indicator = this.indicator({
message: '検索中',
target: document
}).show();
// 画面で選択されたカテゴリ
var category = $('#select-category option:selected').val();
var $resultDiv = this.$find('#resultList');
var that = this;
var count = 0;
$resultDiv.empty();
this.itemSearchLogic.getItemList(category).done(function(resultData) {
that.view.append($resultDiv, 'template1', {listData: resultData});
indicator.hide();
}).fail(function(errMsg) {
alert('取得に失敗しました' + errMsg);
indicator.hide();
}).progress(function(total) {
indicator.message('検索中' + ++count + '/' + total);
});
}
};
h5.core.controller('#container', itemSearchController);
- getItemList()の戻り値にprogressメソッドを使用して、progressコールバックを登録しています。
- progressコールバックは完了した処理の件数が画面に表示される処理を実行します。
動作確認
非同期処理の待ち合わせ
h5.async.when()を使うと、引数に渡した複数のプロミスオブジェクトがresolve()されるのを待つことができます。$.when()と使い方は同じです。引数にプロミスオブジェクトを可変長で複数取ります。また、$.when()と違い、引数にプロミスオブジェクトの配列を取ることができます。
APIドキュメントh5.async.when
実装例
ロジックの一部
// _getDepart(), _getReturn(), _getHotel() はそれぞれ非同期処理を行ってプロミスを返します。
// 3つの非同期が終わってから実行する処理を、getPlanList()のdoneコールバックに記述できます。
getPlanList: function() {
// 3つのメソッド呼び出しを待ち合わせる
// 全ての非同期処理が完了してからdoneコールバックが呼ばれる。
return h5.async.when(this._getDepart(), this._getReturn(), this._getHotel());
},
コントローラの一部(getPlanListの呼び出し)
function(depatFlightList, returnFlightList, hotelList) {
that.log.debug("データの取得が完了しました");
});
非同期処理のチェーン
ある非同期処理が終わってから、次の非同期処理を行いたい場合は、then()を使います。
(jQuery1.8未満の場合はpipe()を使います。)
実装例
// thenの中で返したプロミスオブジェクトの結果を待ってから、
// 次のメソッドチェーンで登録した処理が実行される。
return h5.ajax(result.url);
}).then(function(){
// h5.ajax(result.url)が成功した時の処理を記述
});
commonFailHandler
hifiveのDeferredオブジェクトは、jQueryのDeferredオブジェクトと同様の挙動です。ただし、hifiveで生成したDeferredオブジェクトオブジェクトは共通のエラーハンドラ(commonFailHandler)を設定することができます。
commonFailHandlerは、failハンドラを設定していないDeferredオブジェクトが失敗した場合に、共通のfailハンドラとして実行されます。failハンドラの設定されている非同期処理が失敗した場合は実行されません。
commonFailHandlerの設定は、h5.settings.commonFailHandlerにハンドラ(関数)を設定します。
// 共通のエラー処理
alert('エラーが発生しました');
}
h5.ajax('hoge').done(function(){
// 成功時の処理
});
// 非同期処理が失敗したら共通のエラー処理が実行される
h5.ajax('hoge').fail(function(){
// 失敗時の処理
});
// failハンドラが登録されている場合は失敗しても共通のエラー処理は実行されない
h5.ajax('hoge').always(function(){
// 終了時(成功時または失敗時)の処理
});
// alwaysを使ってfail時の処理が記述されている場合も共通のエラー処理は実行されない
この共通のfailハンドラは、hifiveが生成するDeferredオブジェクトに共通するfailハンドラとなります。h5.async.deferred()や、h5.ajax()等で生成されるDeferredオブジェクトなどです。$.Deferred()で生成した非同期オブジェクトへは適用されません。
commonFailHandlerについての詳細は以下をご覧ください
hifiveにおける例外処理 » commonFailHandler
よくある質問
ロジックのAPIの粒度はどう考えるべき?
1つの非同期処理は1メソッドとした方が良いでしょう。
Deferredを使ってさえいれば、非同期処理の順番が変わったり、間に別の非同期処理を追加しなければならなくなった場合でも柔軟に対応できます。
処理の進行途中で画面に反映させたい場合はどうすればいい?
Deferredにはnotify()というメソッドがあり、最終的な完了通知の前に進捗を通知することができます。
Deferredでnotify()メソッドを呼ぶとPromiseのprogress()で登録したコールバック関数が呼ばれるので、
この仕組みを利用して画面を書き換えるとよいでしょう。
notify()メソッドは引数にオブジェクトをとることができ、渡したオブジェクトは
progress()でセットしたコールバック関数の引数に渡されるので、データに基づいた画面更新も可能です。
最終的な処理完了までに非常に時間がかかる・データ量が多くなる場合はどうすればいい?
progress()等を使うことで途中でコールバックを呼べるので、ロジックのAPIとしては「処理したいものがたくさんあるときは、配列やオブジェクトで一括で渡し、必要に応じてprogress()する」ようにするのが良いでしょう。
次のステップ⇒チュートリアル07.テスト