promiseによる非同期処理とbinding – Ember.js入門(21)
渡辺です。
Ember.jsでは、サーバサイドからRESTful APIを利用してモデルの情報を取得する構成がスタンダードです。 カスタマイズすることで他の構成をとることも可能ですが、基本的にはEmber.jsの作法に従った方がよいでしょう。 その方が開発効率も良く、はまりにくいですし、情報も多く集まります。 この辺りの空気感はRuby on Railsに通じるものがあると思います。
なお、「Ruby on Railsのクライアントサイド版」と言われることがありますが、それは大きな誤解です。 あくまでクライアントサイドのフレームワークであり、設計思想は全くことなります。
さて、今回はRESTful APIでモデルを取得した場合の挙動について解説します。 Routeで適切なモデルを取得するコードを定義すれば、テンプレート(View)で自然にレンダリングされるわけですが、サーバサイドプログラムとは仕組みが全く異なります。 言い換えると、サーバサイドプログラムのような感覚でクライアントサイドアプリケーションを記述できるのがEmber.jsの特徴なのです。
モデルのコントローラ/ビューへの関連付け
Ember.jsではRouteオブジェクトのmodelメソッドで、そのRouteに関連するモデルを取得します。 Routeに関連するモデルが関連付くことで、対応するコントローラ/ビューにもモデルが関連付けられます。
例えば、次のサンプルコードではItemsRouteのmodelメソッドで、Itemモデルの一覧を取得しています。
App.ItemsRoute = Ember.Route.extend({ model: function() { return this.store.find('item'); } });
findメソッドが実行されると、RESTful APIが実行され、モデルの配列を返します。 これはサーバサイドプログラムであれば、RDBにSQLを発行しモデルを取得する処理です。 しかし、サーバサイドプログラムとは異なり、findメソッドで実行されるHTTPリクエストは非同期処理として実行されなければなりません。
もし、同期処理で実装しているのであれば、findメソッドはRESTful APIのレスポンスを待つ必要があります。 すると、処理がブロックされていまい、GUIアプリケーションでありがちな、画面がフリーズしたような状態になるでしょう。
Ember.jsではこの非同期処理を行わなければならない部分を、裏側でイイ感じに処理してくれます。
jQueryのajaxメソッドの非同期処理
ここで、典型的な非同期処理の実装を確認しておきます。
例えば、jQueryでajaxメソッドを実行する場合、次のサンプルコードのようにajaxリクエストが成功した場合の処理をコールバック関数として登録します。
$.ajax({ url: END_POINT_URL + '/items', type: 'GET', dataType: 'json', success: function(atr) { // リクエストが成功した場合の処理 } });
このように非同期処理がコールバック関数として定義されるため、時としてコールバックのネストなども発生し、コードが複雑になります。 コールバック地獄となったコードは可読性も悪く、苦労した経験がある人も多いのではないでしょうか。
Ember.jsでは、基本的には非同期処理をコールバックで記述する必要はありません。 綺麗なコードを維持できることは大切なことです。
promiseオブジェクトとbinding
もう一度、先ほどのコードを確認しましょう。
App.ItemsRoute = Ember.Route.extend({ model: function() { return this.store.find('item'); } });
storeのfindメソッドにはコールバック関数は指定していません。 かといって、RESTful APIのレスポンスを待つこともありません。 裏側でajaxリクエストを行いますが、findメソッドは即時にpromiseオブジェクトを返します。
メソッドは即時にpromiseオブジェクトを返すため、処理がブロックされることはありません。 このpromiseオブジェクトは、一言で表すならば、後でデータが格納されることが約束されたオブジェクトです。 「後で中身はいれるわー、とりあえず箱だけ返すわー」というイメージです。
Routeの処理が終わると、コントローラが初期化され、ビューがレンダリングされます。 とはいえ、この時点でモデルがロードされている保証はありません。 もしかするとサーバエラーなどでモデルがロードされない可能性もあります。 しかし、ビュー(テンプレート)は次のように定義されます。
<ul> {{#each model}} <li>{{name}}</li> {{/each}} </ul>
モデルの一覧が取得されていない状態では、modelのサイズは0です。 したがって、商品の一覧は表示されません。
非同期処理でajaxリクエストが処理されモデルがロードされると、例えばmodelのサイズが4になります。 すると、バインディングされていたプロパティが変更されたことがビューに通知され、再レンダリングが走ります。 したがって、eachで繰り返し処理が実行され、各商品のnameがリスト表示されるわけです。
このように、Ember.jsではモデルの取得を非同期でやりつつ、バインディングの仕組みと組み合わせる事で、可読性が高くレスポンシブルなGUIを記述できるのです。 サーバサイドではRDBからのレスポンスを待ち、その結果をレンダリングする以外の方法はありませんが、クライアントサイドではその考え方は通用しません。
promiseパターン
このように、Ember.jsのDataStoreでは promise パターンが採用されています。 promiseとは、時間のかかる処理が実行され、かつその処理が成功するか失敗するか解らないような状況での結果を返すパターンです。 折角ですので、もう少しだけ理解を深めておきましょう。
PromiseProxyMixin
Ember.jsでは、promiseオブジェクトとして、配列であるPromiseArrayオブジェクト、単一データであるPromiseObjectオブジェクトがあります。 そして、PromiseArrayクラスもPromiseObkjectクラスもPromiseProxyMixinをmixinしています。
promiseオブジェクトの状態
promiseオブジェクトは、処理中(isPending
)・正常完了(isFullfilled
)・異常中断(isRejected
)・完了(isSettled
)といった状態を持ちます。
代表的な使い方としては、次のようにモデルのロードに時間がかかる場合のLoadingメッセージを表示することです。
{{#if isPending}} loading... {{else}} <ul> {{#each model}} <li>{{name}}</li> {{/each}} </ul> {{/if}}
コールバック関数で切り替えていた人にとっては「なんて便利な…」と感じるかと思います。
初期完了時のコールバック処理
ビューのレンダリングについては、原則としてバインディングの機能を利用すればコールバック関数は不要です。
もし、処理完了時になんらかの処理を行いたい場合は、then
メソッドを使ってコールバック関数を登録することができます。
App.ItemsRoute = Ember.Route.extend({ model: function() { var promiseArray = this.store.find('item'); promiseArray.then(function() { console.log('loaded', promiseArray); }); return promiseArray; } });
他にもエラー発生時のコールバック関数としてcatch
、成功失敗どちらでも実行させるコールバック関数としてfinally
が用意されています。
まとめ
このように、Ember.jsでは非同期処理を意識せずに記述できるのが大きな特徴です。 しかし、裏側でどんな挙動をしているのかは理解しておかなければ、あっという間に嵌まりますので注意してください。 また、jQueryでコールバック地獄に悩んでいるのであれば、試してみてはどうでしょうか?