実践入門 Ember.js

第2回URLと画面表示(RoutingとTemplates)

前回はEmber.jsの概要を解説しました。Ember.jsの歴史から今後の展望、そして動作環境を構築して画面の表示ができることまでを示しました。
今回からは簡単なアプリケーションを作成しながら、Ember.jsの構成要素について一つひとつ見ていくことにしましょう。

今回のテーマはRoutingTemplatesです。
なぜこの2つから解説を始めるかというと、RoutingとTemplatesはEmber.jsアプリケーションの入口と出口に相当するものだからです。

今回のゴールは次のようなEmber.jsアプリケーションの仕組みを理解することです。

Ember Starter Kit

前準備

さて、さっそくアプリケーションの作成……といきたいところですが、その前に少しだけ前準備をしておきましょう。

前回からのアップデート

前回の記事からのEmber.jsのアップデートについて簡単に解説します。2014年12月8日に新しいバージョンである1.9.0がリリースされました[1]⁠。Ember.js 1.9.0では、依存するHandlebarsのバージョンが1.3.0から2.0.0に変更されました。そこで、あらためて環境の準備から始めます[2]⁠。

ファイルの準備

  1. まずはEmber.jsを使うために必要なライブラリをダウンロードします。

  2. HTMLとJavaScriptファイルを作成します。

    index.html
    <!DOCTYPE html>
    <html>
      <head>
        <script src="libs/jquery-2.1.1.min.js"></script>
        <script src="libs/handlebars-v2.0.0.js"></script>
        <script src="libs/ember.js"></script>
        <script src="app.js"></script>
      </head>
      <body>
      </body>
    </html>
    app.js
    App = Ember.Application.create();
  3. 次のディレクトリ構成で配置します。

    .
    ├─⁠─ app.js
    ├─⁠─ index.html
    └─⁠─ libs
        ├─⁠─ ember.js
        ├─⁠─ handlebars-v.2.0.0.js
        └─⁠─ jquery-2.1.1.js

開発ツールのインストール

Ember.jsのデバッグツールであるEmber Inspectorをインストールします。

この拡張を入れると、ブラウザの開発者ツールに"Ember"タブが増えます。

Ember Inspector
画像

このタブでは、Ember.jsアプリケーションのデバッグやパフォーマンスチェックが行えるようになります。詳しい使い方は、今後の連載の中で少しづつ解説していく予定です。

さて、ここまでで前準備が完了しました。

Templates

では、さっそくTemplatesから見ていきましょう。

Ember.jsでは、アプリケーションで表示する画面はHandlebarsと呼ばれるテンプレート言語を用いて記述します[3]⁠。HandlebarsはHTML中に式を埋め込む形式のテンプレート言語です。

ただ、JavaScriptの任意の式を記述することはできず、Handlebarsで定義されたヘルパーと描画時に与えたオブジェクトのプロパティのみを記述できます。一見すると不便に感じるかもしれませんが、記述内容を制約することでテンプレートとプレゼンテーションロジックの分離が強制されます。Ember.jsはこの特性を利用して、⁠埋め込まれた値が変化したときに自動で追随する」という仕組みを提供しています。

ではHandlebarsの記法を見ていくことにしましょう。

先ほど作成したindex.htmlbodyタグの中に次のタグを追加してください。

<script type="text/x-handlebars" data-template-name="index">
  <h1>{{title}}</h1>

  タグ:
  {{#each tag in tags}}
    <span>{{tag}}</span>
  {{/each}}

  {{!これはコメントです}}

  <p>{{body}}</p>

  書いた人: {{author.name}}
  <div {{bind-attr class=status}}>
    {{publishedAt}} 公開
  </div>

  {{#if author.twitter}}
    (@{{author.twitter}})
  {{/if}}

  <hr>
  記事を修正する:
  {{input value=body}}
</script>

また、このテンプレートに表示するためのオブジェクトを用意しましょう。app.jsに次の内容を追記してください。

App.IndexRoute = Ember.Route.extend({
  model: function() {
    var post = {
      title: '実践入門 Ember.js',
      body: 'Ember.jsについて解説します。',
      tags: ['Ember.js', 'JavaScript'],
      status: 'newly',
      publishedAt: '2014年12月23日',
      author: {
        name: '佐藤竜之介',
        twitter: 'tricknotes'
      }
    };
    return post;
  }
});

これは、変数postが参照するオブジェクトをテンプレートに渡すためのコードです(詳細は次項で解説します⁠⁠。

さて、動かしてみると次のような表示になっているはずです。

Ember Starter Kit

とくにCSSを入れていないのでとても簡素な見た目ですが、ここにはHandlebarsの基本となる要素が含まれています。では順に説明していきます。

<script type="text/x-handlebars" data-template-name="index">

scriptタグにtype="text/x-handlebars"を指定することで、Ember.jsで使うためのテンプレートであることをEmber.jsに知らせています。また、data-template-nameに指定した値がテンプレートの名前になります[4]⁠。Ember.jsがテンプレートを探索する際はこの名前を利用します。

<h1>{{title}}</h1>

{{から}}で囲まれた部分にはオブジェクトのプロパティやHandlebarsのヘルパーを埋め込むことができます。ここではオブジェクトのプロパティであるtitleを埋め込んで表示しています。

{{#each tag in tags}}...{{/each}}

値が配列の場合、eachヘルパーを使うと要素をひとつひとつ取り出せます。この#/で範囲を指定するヘルパーをHandlebarsの用語では"Block Expressions"と呼びます。

{{!これはコメントです}}

{{!}}の範囲はコメントです。HTMLには描画されません。

{{author.name}}

オブジェクトがネストしている場合、.で深い階層の値を参照できます。

<div {{bind-attr class=status}}>

HTMLの属性を埋め込みたい場合はbind-attrヘルパーを使います。下の例のように属性に{{}}を埋め込んだ場合、エラーになってしまい動作しません。

<div class="{{status}}">

これはEmber.jsが提供するHandlebarsでの値の自動更新の実装上の制約です。

ただ、Ember.js 1.10.0から導入予定のHTMLBarsではこの制約はありませんので、上の例のように属性を直接埋め込んでも動作します。

{{#if author.twitter}}

値の真偽で出力を切り替えられます。通常のJavaScriptと同じく0''(空文字)が偽として評価されるほか、空配列も偽として評価されます。ifの他に、値が偽のときに評価されるunlessもあります。そしてそのどちらもelseが指定可能です。

次の2つの例は同じように評価されます。

{{#if isPublished}}
{{else}}
  (この記事は未公開です)
{{/if}}
{{#unless isPublished}}
  (この記事は未公開です)
{{/if}}
{{#unless isPublished}}
  (この記事は未公開です)
{{/if}}

elseは範囲を取らない{{#else}}~{{/else}}とならない)ことに注意してください。

{{input value=body}}

inputタグを生成するヘルパーです。

value=bodyとすることで、inputタグのvalue属性とオブジェクトのbodyプロパティを結びつけています。これによって、ユーザがテキストフィールドに値を入力することで画面に表示されているbodyも追従して変更されます。このような仕組みを「データバインディング」と呼びます。Ember.jsはデータバインディングを全面的にサポートすることで、アプリケーションから決まりきった画面更新のロジックを排しプログラマの負担を軽減してくれます。

以上、Templatesの簡単な説明でした。

Routing

次は Routingを見ていきます。
RoutingはURLとアプリケーションの画面を対応付ける機能です。

例えば、⁠記事の編集ページを開いている」⁠設定のタブを開いている」といった状態をURLで表現することで、JavaScriptで複雑な状態管理をしなくてはいけない状況からプログラマを解放してくれます。

もちろんこれはユーザにとってもメリットがあります。アプリケーションの状態をURLで表現できることは、SPAであっても任意の画面をブックマークできることになります。

RESTfulなWebアプリケーションを設計する際と同様、Ember.jsでSPAを設計する際にはURLを意識するのがよいプラクティスと言えます。

Ember.jsはデフォルトではURLを#からはじまるフラグメントハッシュの形で提供します[5]⁠。Webサーバが返したパスを起点にして、#/以下にEmber.jsアプリケーションのパスを構築します。

RoutingにはRouterRouteという2つの部品が登場します。それぞれについて、サンプルコードを用いて解説します。

Architecture
画像

ここではapp.jsindex.htmlを準備したばかりの状態から始めることにします。

app.js
App = Ember.Application.create();

// Router
App.Router.map(function() {
  this.route('welcome', {path: '/'});
});


// Route
App.WelcomeRoute = Ember.Route.extend({
  model: function() {
    return {
      name: 'さとう'
    };
  }
});

テンプレートはこちらです。index.htmlbodyタグの中に記述してください。

<script type="text/x-handlebars" data-template-name="welcome">
  こんにちは, {{name}} さん
</script>

では、これらについて順番に説明していきます。

Router

Routerは今ブラウザで表示しようとしているURLについて、対応するRouteを探し出す役割を担います。Router.map()メソッドの中でthis.route(route名, オプション)の形式でRouteとURLの対応づけを行います。

App.Router.map(function() {
  this.route('welcome', {path: '/'});
});

この例では、WelcomeRoute/というURLに対応付けています[6]⁠。route名とは、Routeのクラス名からRouteを削って小文字にしたものです。今回の例だと、WelcomeRouteに対応するroute名はwelcomeです。※7⁠。

もしURLとroute名が同じであれば、pathの指定を省略できます。

App.Router.map(function() {
  this.route('welcome');
});

この記述だと、#/welcomeというURLでアクセスされた場合WelcomeRouteが有効になります。

また、Routerはデフォルトで#/IndexRouteの対応を持っています。したがって{path: '/'}を省略した場合、IndexRouteを自分で定義することで#/の画面に表示するオブジェクトを用意できます(Templatesの例で取り上げたサンプルがまさにこれです⁠⁠。

Route

Routerによって処理を引き渡されたRouteは、自身に関連付けられたモデルオブジェクトとテンプレートを使って画面を描画します[8]⁠。

App.WelcomeRoute = Ember.Route.extend({
  model: function() {
    return {
      name: 'さとう'
    };
  }
});

modelメソッドの戻り値がこのRouteに対応するモデルオブジェクトになります。Routeはモデルオブジェクトをテンプレートに渡して画面を描画します。このとき利用されるテンプレートはroute名と同じものが使われます。この例の場合は、data-template-name="welcome"のテンプレートが使われます。

通常だとモデルオブジェクトはサーバサイドのAPIを呼び出して取得したデータを利用することが多いのですが、この例ではサンプルとして固定のシンプルなオブジェクトを返しています。

デバッグオプション

さて、実際にアプリケーションを作成する際には画面の数だけRouteを作成することになります。そうすると、どんなRouteが定義されていて今どのRouteが有効になっているのか知りたくなることでしょう。

そんなときのために、Ember.jsにはRoutingをデバッグするためのオプションが用意されています。

app.jsの以下のように書き換えてください:

Before:
App = Ember.Application.create();
After:
App = Ember.Application.create({
 LOG_TRANSITIONS: true,
 LOG_VIEW_LOOKUPS: true
});
  • LOG_TRANSITIONS … Route間の遷移をログ出力します(参考: LOGGING ROUTE CHANGES
  • LOG_VIEW_LOOKUPS … 画面表示に使用するテンプレートをログ出力します(参考: LOG VIEW LOOKUPS

これらのデバッグオプションを有効にしておくことで、⁠なんだかわからないけど画面に何も表示されない」といった場合に調査のヒントにできます。

これで、開発者ツールのコンソールに次のようなメッセージが表示されるようになります。

画像

テンプレート

<script type="text/x-handlebars" data-template-name="welcome">
  こんにちは, {{name}} さん
</script>

Routemodelメソッドで返したオブジェクトを表示します。これは先ほどTemplatesの項で解説した通りです。

Route間の遷移

さて、ここまでだとRoutingのありがたみをあまり感じません。なぜならアプリケーションに対して何も操作を行っていないからです。実際のアプリケーションであれば、⁠ボタンをクリックして設定画面を開く」とか「リンクを押して詳細を表示する」といった操作を行うでしょう。

そして、画面遷移は有効になっているRouteが切り替わることで実現されます。では複数のRouteを用意してEmber.jsでの画面遷移を体験してみましょう。

まずはRouterとRouteを用意します。

App.Router.map(function() {
  this.route('posts', {path: '/'});
  this.route('post', {path: '/post/:id'});
});

var posts = [
  {
    id: 1,
    title: 'Ember.js の世界',
    body: 'Ember.js の全体像をご紹介します。'
  }, {
    id: 2,
    title: 'Ember.js の秘密',
    body: 'Ember.js のとっておきをご紹介します。'
  }, {
    id: 3,
    title: 'Ember.js の奇妙な冒険',
    body: 'Ember.js の独特な世界観をご紹介します。'
  }
];

App.PostsRoute = Ember.Route.extend({
  model: function() {
    return posts;
  }
});

App.PostRoute = Ember.Route.extend({
  model: function(params) {
    var found = posts.filter(function (post) {
      return post.id === Number(params.id);
    });

    return found[0];
  }
});

次はテンプレートを用意します。

<script type="text/x-handlebars" data-template-name="posts">
  <h1>記事一覧</h1>

  <ul>
  {{#each post in model}}
    <li>
      {{link-to post.title "post" post}}
    </li>
  {{/each}}
  </ul>
</script>

<script type="text/x-handlebars" data-template-name="post">
  <h1>{{title}}</h1>
  <p>
    {{body}}
  </p>
  {{link-to "戻る" "posts"}}
</script>

完成イメージは次の通りです。

URLが更新される様子を確認するために、別のタブで開いてみてください。

完成イメージ別のタブで開く
Ember Starter Kit

記事のタイトルをクリックして詳細を表示すると、URLにidが含まれていることが確認できます。これで、画面を更新しても同じ画面を表示できます。

これは非常に簡単な例ですが、⁠SPAでの画面にURLを割り当てることができる」ことの雰囲気を感じてもらえたでしょうか?

新登場のヘルパー

では、動作について確認できたところで新しくでてきたヘルパーを紹介します。

link-to

link-toはaタグを出力するヘルパーです。

これを使うと与えられたオブジェクトから生成したURLがhrefに設定され、Route間の遷移ができるようになります。

{{link-to "次へ" "hello" model}}

link-toの引数は次のようになっています。

link-to (テキスト) (route名) (オブジェクト) (オプション)
(テキスト)

生成されるタグのテキストです。

{{#link-to (route名) (オブジェクト) (オプション)}}
  テキスト
{{/link-to}}

と書くこともできるので、テキスト部分にHTMLを含めたい場合こちらを使う方が記述しやすいです。

(route名)

this.route()で指定したRouteの名前です。this.route(name, {path: path})の形でRouteを定義している場合はnameの部分です。

(オブジェクト)

テンプレート中でのmodelは、Routeのmodelメソッドで生成されたオブジェクトを指します。link-toにオブジェクトを渡すと、this.route()で定義したpathにしたがってURLが生成されます。this.route('post', {path: '/post/:id'})の場合、model.idがURL生成に利用されます。

(オプション)

ここにはlink-toが生成するHTMLのためのオプションを渡すことができます(省略可能⁠⁠。指定可能なオプション一覧はAPIリファレンスの"PROPERTIES"を参照してください。

例えば、以下のような使い方ができます。

{{link-to post.title "post" post target="_blank"}}

アプリケーションのレイアウト

いくつもの画面を作成していった場合、⁠ヘッダやサイドバーが共通しているのでどこかにまとめておきたい」というケースが想定されます。そんなときはアプリケーション全体で共通するレイアウトを定義できます。

<script type="text/x-handlebars">
  <header>
    <h1>Ember.js</h1>
  </header>

  <aside id="sidebar">
    ...
  </aside>

  <main>
    {{outlet}}
  </main>

  <footer>
    ...
  </footer>
</script>

ここでのポイントは2つあります。

  1. data-template-nameを指定していない

    data-template-nameを省略すると、アプリケーションでの共通レイアウトとして設定されます[9]⁠。

  2. {{outlet}} ヘルパーを使っている

    画面遷移を行うと、共通のレイアウト中の{{outlet}}に各Routeで描画されたテンプレートが差し込まれます。

この共通テンプレートを使うことで、Routeで使われるテンプレートをシンプルに保つことができます。

以上でRoute間の遷移についての解説は終わりです。

まとめ

今回はEmber.jsアプリケーションを作るための基礎の部分であるRoutingとTemplatesを解説しました。

次回はもう少し実践的な例を用いて、この2つを深く解説します。

おすすめ記事

記事・ニュース一覧