前回はURLと画面表示の基礎を解説しました。今回はEmber.jsでの画面遷移について、前回の内容を踏まえた上でもう少し踏み込んだ解説をします。
Ember.jsの「画面遷移」とはページ全体を再描画するのではなく、画面を部分的に書き換える動きをします。これがどういうことかを、例えばヘッダとサイドメニュー、メインコンテンツの表示部分があるアプリケーションを例にあげて考えてみましょう。
サイドメニューをクリックしてメインコンテンツを切り替えるという挙動を考えた場合、一般的なWebサイトであれば画面遷移が発生しヘッダ・サイドメニューを含めた画面のすべてが再描画されることでしょう。その一方でEmber.jsアプリケーションの場合はメインコンテンツのみが書き換えられます。
さて、それではさっそく今回の解説を進めていきましょう。今回は次のようなEmber.jsアプリケーションの仕組みを理解することを目標にします。
前準備
まずはEmber.jsを使うために必要なライブラリをダウンロードします。
次のindex.html
、app.js
を作成します。
また、style.css
という名前で、中身は空のCSSファイルを作成してください。
これらを以下のディレクトリ構成で配置します。
以降、特に指定のない場合、JavaScriptはapp.js
に、Handlebarsのテンプレートはindex.html
のbodyタグの中に、CSSはstyle.css
に記述することにします。
Routing
前回の解説では、アプリケーションの画面1つひとつがRoute
に対応していると解説しました。では、画面を部分的に書き換えるというのはどう実現すればよいのでしょうか?
実は、Ember.jsではRoute
を階層化することができ、入れ子になった子のRouteを切り替えることで画面を部分的に更新できます。
まずはルーティングを定義しておきましょう。
あわせてテンプレートを定義しておきます。
また、動作確認用のデータとRouteを定義します。ここではJavaScriptファイルの中にデータを埋め込みましたが、実際にアプリケーションを作成する際はサーバからデータを取得してくることになるでしょう。この方法については後ほど解説します。
見栄えを整えるために、CSSも作成しておきます。
では、この状態でどのようなURLが定義されるのかEmber Inspectorを使って確認してみましょう[1]。
いくつか自動で生成されるRouteがありますが、これについては後ほど解説します。ここでは次のRouteに着目してください。
これらのRouteについて詳しく見ていきます。
PostsRoute
:/#/posts
で始まるURLにアクセスされた際にアクティブになります。記事一覧を表示します。以下2つのRouteの親です。
PostsIndexRoute
:/#/posts
にアクセスされた際にアクティブになります。まだ記事が選択されていない状態なので、「記事を選択してください。」というメッセージを表示します。
PostsShowRoute
:/#/posts/:post_id
にアクセスされた際にアクティブになります。一件の記事を表示します。
特定のRouteがアクティブになる際、まずその親のRouteがアクティブになってから子のRouteがアクティブになります。これがどういうことか具体例で説明します。
/#/posts
にアクセスされた場合
対応するRouteはPostsIndexRoute
なので、まずはその親のPostsRoute
がアクティブになりテンプレートを描画します。
次に、PostsIndexRoute
がアクティブになり、親であるPostsRoute
のテンプレート中のoutlet
にPostsIndexRoute
自身のテンプレートを描画します。
/#/posts/:post_id
にアクセスされた場合
対応するRouteはPostsShowRoute
なので、まずはその親のPostsRoute
がアクティブになりテンプレートを描画します。
次に、PostsShowRoute
がアクティブになり、親であるPostsRoute
のテンプレート中のoutlet
にPostsShowRoute
自身のテンプレートを描画します。
前回の記事で、アプリケーションのレイアウトを指定する際にはapplication
テンプレートでoutlet
を使うと解説しましたが、その正体はここで紹介した仕組みと同じです。実はすべてのRouteの親を辿ると、ApplicationRoute
に行き着くのです。
ここで作成したアプリケーションを実際に動かしてみて、画面とURLが対応していることを確認してみてください。
さて、イメージが確認できたところでコードを細かく見ていきましょう。
Route / Resource
まずはRouterで記述可能なroute()
とresource()
について解説します。
さきほどのルーティングの例ではreoute()
とresource()
を使っていました。これらは似たような機能を持つメソッドなのですが、 実はURLに対応させるRoute名に違いがあります。
route()
だと階層化した際に親Routeの名前を引き継ぐのに対し、resource()
では親の有無に関係なくRoute名が決まります。
では、route()
とresource()
はどうやって使い分ければよいのでしょうか?
公式ガイドによると、URLの対象が名詞である場合はresource()
を、動詞もしくは形容詞の場合はroute()
を使うべきであると記述されています。
これにしたがって今回のサンプルコードでは、次のように利用しています。
- 「
posts
(記事)」(名詞)には resource()
- 「
show
(表示する)」(動詞)には route()
Nested Route
Router.map
でresource()
の入れ子としてroute()
を呼び出しています。こうして入れ子にすることで、Routeの親子関係を定義できます。
ポイントは次のとおりです。
Route はいくつでも階層化することができます。
また、route()
/ resource()
を任意の組み合わせで入れ子にすることができます。
テンプレート名(data-template-name
) は 親子関係にある Route 名を /
で区切った名前が対応します。
this.modelFor('Route 名')
では、親の Route の model()
を参照できます。
同一の親 Route 以下での画面遷移であれば model()
の呼び出し結果は再計算されないので、高速に画面を描画することができます。
実は今回扱うアプリケーションは、前回扱ったアプリケーションとほとんど同じような機能を持っています。
- ブログ記事一覧を表示できる
- ブログ記事の詳細を表示できる
大きく違うのは、今回のアプリケーションではRouteを階層化して常に記事一覧を表示しているところです。ではどういう場合にRouteを階層化させるとよいのでしょうか?
それは、アプリケーションの画面設計次第です。画面のパーツで共通で利用したい部分があればRouteを階層化させますし、切り替えたい場合には階層化しません。
Active
Routeがアクティブになっているとき、link-to
ヘルパが生成した「そのRouteへ画面遷移を行うリンク」にはactive
というCSSのクラスが自動で付与されます。このactive
クラスにスタイルをあてることで、現在表示されているリンクをハイライトすることができます。
今回のサンプルでもそのようなCSSを利用しています。
また、このactive
というクラス名はカスタマイズ可能です。次のようにactiveClass
オプションを利用して、任意のクラス名を指定することができます。
上記の例では、PostsShowRoute
がアクティブなときにはcurrent
というクラスが付与されます。
Loading Data
さて、ここまでの例では手元にデータを用意して動作確認しました。しかし実際にアプリケーションを組み立てるとなると、リモートサーバから取得したデータを取り扱うというケースが多くなることでしょう。この項ではその方法を紹介します。
さきほど用意した手元のデータを、JavaScriptファイルから抜き出してJSONファイルとして保存します。
これからこのJSONをXHRで取得するようコードを書き換えます。XHRではhttpもしくはhttpsプロトコルを利用する必要があるため、ここでは以下のURLに保存したJSONを利用します。
このURLはみなさまのお手元からも利用できます[2]。
では、PostsRoute
を書き換えましょう。
そしてブラウザをリロードしてみましょう。今までと変わらず記事一覧が表示されているでしょうか?
ブラウザの開発者ツールを利用して、JSONにHTTPリクエストが発行されていることを確認してみましょう。
Google Chrome 39だと、次のようにHTTPリクエストが発生していることが確認できます。
ところで、jQueryでJSONを扱うコードを書いたことがある方ほど、このコードに違和感を覚えたのではないでしょうか? なぜなら本来サーバから取得したJSONを扱うためには、次のように$.getJSON
の引数として渡したコールバックの中からデータにアクセスする必要があるからです。
では、ここで起こっていることを詳しく見てみましょう。
まず、$.getJSON()
の戻り値はjqXHR
オブジェクトです。このjqXHRオブジェクトはPromiseのインターフェースを実装しています。Promiseとは非同期を扱うためのパターンのひとつで、非同期処理開始時にコールバックを与えるの代わりに、任意のタイミングでコールバックを設定できます。今回は詳しく踏み込みませんので、詳しくはJavaScript Promiseの本をご覧ください。
コールバックを与えていた例をPromiseのインターフェースで書き直すと、次のように書けます。
そしてEmber.jsのRouteにはmodel()
でPromiseが返されると、非同期処理を待ってから画面遷移するという仕組みがあります。この仕組みのおかげで、データが手元にある場合でもリモートサーバから取得してくる場合でも、これらを特別に区別することなくデータを扱うことができます[3]。
さて、ここまででリモートのデータを扱う方法をご紹介しました。しかし、リモートのサーバを相手にするとなると、通信に時間がかかったり失敗したりした場合のことを考えなくてはいけません。そんなとき、Ember.jsではどのようにすればよいのでしょうか?
Error handling
実は、そんな状況に対処する方法がEmber.jsには用意されています。
Substates
すべてのRouteにはloading
とerror
という子Routeが自動的に用意される仕組みがあります。これはEmber.jsの用語でSubstatesと呼ばれ、次のタイミングでアクティブになります。
loading
:Routeのmodel()
が実行されている間アクティブになります。
error
:Routeがテンプレートを描画するまでの間なんらかの例外が発生した場合にアクティブになります。
Ember.jsの命名規約で、それぞれRoute名にLoading
/ Error
という名前を追加したものがSubstatesのRoute名になります。例えば、PostsRoute
の場合はPostsLoadingRoute
/ PostsErrorRoute
です。また、一番親のRouteであるApplicationRoute
に対応するSubstatesはLoadingRoute
/ ErrorRoute
です。
この仕組みを利用すれば、modelを読込中であったりエラーが発生したという状況を開発者がハンドルすることができます。
では、それぞれの動作を確認してみましょう。
Loading Substate
次のRouteを作成してください。
ここで利用しているactivate()
メソッドは、Routeが有効になった際に実行されるメソッドです。ログの出力や画面表示に際して前処理を行いたい場合に利用します。
ここまでのJavaScriptを記述したところでアプリケーションを動かしてみましょう。開発者コンソールのログに「読み込み中です」と表示されたでしょうか?
コンソールに出力されるだけだと開発者にしかありがたみはないので、画面にも見込み中であることを表示しましょう。次のテンプレートを作成してください。
アプリケーションをリロードすると、JSON を取得している間は「現在データを読込中です…」が表示されていることを確認できます。
Error Substate
では、エラー時の挙動も確認してみましょう。次のRouteを定義してください。
次はこのErrorRoute
の挙動を確認するために、JSONのリクエスト先を変更しておきましょう。
/error.json
に応答できるファイルは存在しないので、サーバからはステータスコード404が返ってくるはずです。
この状態でアプリケーションを動かしてみると、コンソールにメッセージが出力されていることを確認できます。
次はエラーが発生したことを画面に表示しましょう。次のテンプレートを作成してください。
アプリケーションを動かしてみると、JSONの取得に失敗して「おや、何か様子がおかしいです。」と表示されていることを確認できます。ここで「もう一度読み込む」のリンクををクリックすると、PostsRoute
がアクティブになり、再度JSONの取得を試みることができます。ただ、先ほどJSONの URLを無効なものに書き換えてしまったため、何度試しても失敗するだけです。
ErrorRoute
の動作が確認できたところで、PostsRoute
のmodel()
を元に戻しておきましょう。
また、特にRouteで行いたい処理がなくエラー画面さえあれば十分だという場合、Route定義を省略してテンプレートだけを定義しておくことが可能です。独自処理が不要な場合に記述を省略できるのは、その他のRouteと全く同じですね。
今回の例ではPostsRoute
でJSONを取得しているため、このRouteのloading
に対応しているLoadingRoute
を利用しました。もしPostsShowRoute
で一件の記事のJSONを取得するような場合であれば、PostsLoadingRoute
を使うこともできます。
Substateでは子のイベントはハンドルできますが、親または兄弟のRouteのイベントはハンドルできません。また、親でハンドルされなければ、さらにその親のRouteでハンドルすることができます。
今回の解説の最初でRoute定義を確認した際に見慣れないRouteがいくつかありましたが、これらがここで紹介したRouteです。
Events
Substatesの仕組みだと手軽にエラーをハンドリングできる一方で、エラーの種類によって処理を切り替えたいというケースには不向きです。そのような場合に利用できる仕組みがEventsです。
Routeは特定のタイミングでloading
/ error
イベントが発火します。このイベントをハンドルするためにはactions
というプロパティを設定し、そこでerror
/ loading
というイベントハンドラを定義します。
error
イベントハンドラの第一引数には、エラーオブジェクトまたはPromiseがエラーとみなしたオブジェクトが渡ってきます。$.getJSON()
の場合、HTTP リクエストに失敗するとjQueryが提供するjqXHR
オブジェクトが渡されます。そこで次のようにして、HTTPステータスを参照してエラー時の処理を切り替えることができます。
ここで利用しているRoute
のtransitionTo('route 名', [モデル...])
メソッドは画面遷移を行うためのメソッドです。テンプレートで利用しているlink-to
ヘルパと同じ使い方ができます。
ここでは次のようなテンプレートを定義することで、HTTPリクエストが404だった場合のエラーを特別扱いすることができます。
またEventsはSubstatesと同じく、Routeでハンドルされなかったイベントはその親のRouteでハンドルすることが可能です。
Location
ここまで、Ember.jsアプリケーションに割り当てられたURLは#
から始まるフラグメントハッシュと呼ばれるものでした。ただ、状況によっては#/posts
のようなURLではなく、/posts
のようなURLでアプリケーションを管理したいという場合があります。
そのようなときは、次のようにHistory APIを利用して/
で始まるURLを利用できます。
ただ、この場合にEmberアプリケーションで管理しているURLを完全にサポートするためには、Webサーバ側でも対応が必要になります。
また、Webページ中にEmberアプリケーションを埋め込んで利用したいため、Ember.jsアプリケーションの画面遷移の際にURLを更新してほしくないこともあります。
その場合には次の設定の行うことで、URLを更新せずにアプリケーションで画面遷移を行うことができます。
まとめ
今回はEmber.jsアプリケーションでの画面遷移について解説しました。Ember.jsでの画面遷移とは、画面を部分的に更新することで実現されます。また、Routeを適切に定義しておくことで、画面の状態をURLで表現可能になります。
次回はControllerを扱って一時データの保持とユーザの操作を受け取る方法を解説する予定です。