Routing

5

翻訳の進捗

本章で学ぶこと:

  • Meteorでのルーティングを学びます。.
  • 固有のURLで投稿ディスカッションページを作ります。(???)
  • どうやって適切にURLをリンクするのか学習します。
  • 今のところ、投稿の一覧表ができたので(最終的にはユーザーが投稿するようにします)、 ユーザー同士で投稿を議論できる個別の投稿ページが必要です。

    このページをパーマリンクを通じて、アクセスできるようにしたいと思います。  パーマリンクとは http://myapp.com/posts/xyz の形をした URL で、それぞれの投稿は固有のものです。( xyz の部分は MongoDB の _id 識別子です)

    ブラウザーの URL バーの中に何があるのか調べてそれに対応して正しいコンテンツを表示するために、何らかのルーティングが必要となります。

    Iron Router パッケージを入れる

    Iron Router は Meteor アプリのルーティングに特化したパッケージです。

    Iron Router はルーティング(パスの設定)に役立つだけではなく、  設定をしたパスにアクションを割り当てるフィルターも処理します。  さらに、どのパスがどんなデータにアクセスするのか制御するサブスクリプションも扱います。  (注釈:Iron Router の一部は Discover Meteor の著者の一人であるトム・コールマンによって開発されました。)

    最初に、Atmosphere からパッケージをインストールしましょう。

    $ mrt add iron-router
    
    Terminal

    このコマンドで iron-router パッケージをアプリにダウンロードしてインストールをしたので、すぐに使える状態となります。  パッケージを使えるようになる前に Meteor アプリを再起動する必要があるかもしれません。 (ctrl+c でプロセスを中止して、再び起動するために meteor を入力します。)

    ルーティング用語集

    この章では、ルーティングに関する様々な機能を学びます。  もしあなたが Rails などのフレームワークの経験があるなら、  ルーティングの概念についてすでに知っていることでしょう。  もしそうでなければ、ここでちょっとした用語集を用意しました。

    • ルート:ルートとは、ルーティングの基本的な構成要素です。     アプリが URL を見つけたら、どこに行って、何をしたらよいかといった基本的な命令がセットになったものがルートです。

    • パス:パスとはアプリ内での URL のことです。パスは静的な(/termsofservice) や動的な(/posts/xyz)、 (/search?keyword=meteor)といったクエリパラメータもあります。

    • セグメント:パスの様々なパーツのことで、前のスラッシュ(/)で区切られています。

    • フック:フックとはルーティングプロセスの前や後、あるいはルーティングプロセス中に実行させるアクションです。      代表的な例として、ページを表示する前にユーザーが適切な権利を持っているか照合することが挙げられます。

    • フィルター:フィルターとは、一つ以上のルートをグローバルに定義するフックです。  

    • ルートテンプレート: 各ルートはテンプレートを指し示す必要があります。  テンプレートを指定していない場合、ルーターはデフォルトでルートと同じ名前のテンプレートを探します。

    • レイアウト:レイアウトとは、デジタルフォトフレームのようなものだと捉えることができます。  レイアウトはカレントテンプレートをラップするすべての HTML コードを組み入れ、  テンプレートが変化しても同じ状態を保ちます。  

    • コントローラ:時々、多くのテンプレートが同じパラメータで再利用できることに気づくでしょう。  コードを重複させるのではなく、ルーティングロジックをすべて含んだ1つのルーティングコントローラで  ルートを継承することができます。

    Iron Router に関する詳細は、GitHub 上のドキュメンテーションで全文を見ることができます。

    Routing: テンプレートに URL を割り当てる

    今まで、{{>postsList}} のようにハードコードされたテンプレートのインクルードを使ってレイアウトを作ってきました。 そのため、アプリの内容は変化できますが、ページの基本的な構造は常に同じです: つまり、ヘッダーと投稿の一覧表です。

    Iron Router が HTML の タグの中をレンダリングしたものを引き継ぐことで、私たちはこの骨組みから抜け出すことができます。  そのため、私たちは通常の HTML ページのようにタグの中を自ら定義をしません。  その代わりに、{{> yield}} テンプレートヘルパーを持つ特別なレイアウトテンプレートをルーターに指定します。

    この {{> yield}} ヘルパーは特別な動的な範囲を定義します。  動的な部分ではカレントルートに対応するあらゆるテンプレートに自動的にレンダリングします。 (私たちは今後からこの特別なテンプレートを「ルートテンプレート」と指定します。)

    Layouts and templates.
    Layouts and templates.

    ではレイアウトを作って {{> yield} } ヘルパーを追加してみましょう。 はじめに、main.html から HTML の タグの中身を layout.htmlの中のテンプレートに移動させます。

    すると、スリムになった main.html は現在このようになっています。

    <head>
      <title>Microscope</title>
    </head>
    
    client/main.html

    新しく作った layout.html は現在、アプリの外側のレイアウトを組み入れています。

    <template name="layout">
      <div class="container">
      <header class="navbar">
        <div class="navbar-inner">
          <a class="brand" href="/">Microscope</a>
        </div>
      </header>
      <div id="main" class="row-fluid">
        {{yield}}
      </div>
      </div>
    </template>
    
    client/views/application/layout.html

    postsList テンプレートを yield ヘルパーの呼び出しに差し替えたことに気づいたことでしょう。  また、その変更後はスクリーン上になにもないことにお気づきかと思います。  これはまだルーターに / URL で何を処理するか指定していないためです。  そのため、(it=ルーター?)は空っぽのテンプレートを出力しています。

    はじめに、postsList テンプレートに/ URL ルートを割り当てて、以前と同じ状況にします。。  ルートのプロジェクトで /lib directory を作って、その中に router.js を作ります。

    Router.configure({
      layoutTemplate: 'layout'
    });
    
    Router.map(function() {
      this.route('postsList', {path: '/'});
    });
    
    lib/router.js

    これで2つの大事なことを行いました。  1つ目は、私たちが作ったデフォルトのレイアウトをすべてのルートで使うことをルーターに指定しました。  2つ目は、postsList という名前の新しいルートを定義して、/ パスを割り当てました。

    The /lib folder

    /lib フォルダ内に置いたものは、何よりも前に最初に読み込まれることが保証されています。 (スマートパッケージは例外です) そのため、/lib フォルダはヘルパーコードを置く場所として最適です。ヘルパーコードは常に有効とする必要があるためです。

    しかし、注意点が1つあります。 /lib フォルダは /client 、/server 内のどちらにもないので、 /lib フォルダの中のコードはどちらの環境でも有効化されるということに気をつけましょう。

    Named Routes

    ここで少しあいまいな点をクリアにしましょう。  私たちはルートに postsList と名付けましたが、postsList という名前のテンプレートもあります。  すると、どんなことが起こるでしょうか。

    デフォルト状態だと、 Iron Router はルートと同じ名前のテンプレートを探します。  実際には、ルートの名前に基づいてパスを探します。  ルートの定義にパスのオプションを与えたカスタムパスを定義していない場合、  テンプレートはデフォルトで /postsList の URL でアクセスできるということです。

    そもそも、なぜでルートに名前をつける必要があるのでしょうか。  それはルートに名前を付けることで、簡単にアプリ内のリンクを作る Iron Router の機能を使うためです。   最も役立つ機能は {{pathFor}} という Spacebars ヘルパーです。これはあらゆるルートの URL パスの構成要素を返します。

    メインホームリンクが投稿のリストを返すようにしたいので、 静的な / URL を指定する代わりに、Spacebars ヘルパーを使います。 最終的な結果は同じとなりますが、ルーターでルートのパスを変えたとしてもヘルパーは常に正しい URL を出力するため、 私たちはフレキシビリティを得ることができます。

    <header class="navbar">
      <div class="navbar-inner">
        <a class="brand" href="{{pathFor 'postsList'}}">Microscope</a>
      </div>
    </header>
    
    //...
    
    client/views/application/layout.html

    コミット 5-1

    Very basic routing.

    Waiting on Data

    もし現在の状態のアプリをデプロイしたら(あるいは上記のリンクを使ってインスタンスをローンチしたら)、  投稿が現れる前のわずかな間はリストの中身がないことに気づくことだと思います。  これはページが最初に読み込まれるときは posts サブスクリプションがサーバーから投稿データを取ってくるまでの間に表示する投稿がないためです。

    ユーザーに何が起きているのかという視覚的なフィードバックを与えることができれば、よりよいユーザーエクスペリエンスとなって、ユーザーは少しの間待ってくれることでしょう。

    幸運なことに、Iron Router にはそうするための簡単な方法があります。 waitOn サブスクリプションです。

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    Router.map(function() {
      this.route('postsList', {path: '/'});
    });
    
    lib/router.js

    ひとつひとつ見ていきましょう。 最初に、loading template(これからすぐに作ります。)の名前をルーターに与えるために、私たちは Router.configure() ブロックを修正しました。 loading template はアプリがデータを待っている間にリダイレクトを行います。

    次に、posts サブスクリプションを返す waitOn関数を加えました。 最後に、私たちはローディングフックを作りました。 これはユーザーがリクエストしたルートにユーザーを送る前に、 ルーターが posts サブスクリプションを読み込むようにします。

    私たちは waitOn関数をルーターレベルでグローバルに定義しているので、 このシーケンスはユーザーが初めてアプリにアクセスした際に一度だけ起こります。 その後、このデータはブラウザーのメモリにすでに読み込まれ、ルーターは再び待つ必要がなくなります。

    これでルーターがサブスクリプションを処理するようにしたので、 main.js のコードを安全に削除することができます。(main.js は現在何も入っていない状態です。)

    サブスクリプションを待つことは良いアイデアです。それは単にユーザーエクスペリエンスのためだけではなく、データが常にテンプレート内から利用できると問題なく想定できるからです。 基礎的な情報を利用する前に、レンダリングしたテンプレートを処理する必要がなくなります。 そうするにはたいて巧妙な回避方法が必要です。

    また、onBeforeAction フィルターを加えて、Iron Router に組み込まれた loading フックを動作させます。 そして、データを待っている間に loading テンプレートを表示するようにします。

    <template name="loading">
      {{>spinner}}
    </template>
    
    client/views/includes/loading.html

    {{>spinner}} は spin パッケージに含まれている部分です。 この部分はアプリの「外側」から来ているわけですが、通常のテンプレートと同様にインクルードすることができます。

    コミット 5-2

    Wait on the post subscription.

    A First Glance At Reactivity

    リアクティビリティは Meteor の根幹をなす部分ですが、これまで触れてきませんでした。ローディングテンプレートでこのコンセプトを垣間見ることができます。

    データがロードされていないかどうかローディングテンプレートにリダイレクトすることは良いことですが、ユーザーを正しいページにリダイレクトする時をルーターはどうして分かるのでしょうか。(???) 

    今のところはとりあえずリアクティビリティがその役割をしているとだけ言っておきましょう。 このことについてはこの後すぐに学びますので、心配ご無用です!

     特定の投稿にルーティングする

    ここまでで、どのように postsList テンプレートにルーティングするのか見てきました。 ここでは1つ1つの投稿の詳細を表示するためのルートをセットアップしましょう。

    (There’s just one catch?):1つの投稿ごとにルートの定義をするとなると、数百の投稿に対して同じことをすることになり、先に進めなくなってしまいます。 そのため、1つの動的なルートをセットアップして、ルートに私たちが見たい投稿を表示させる必要があります。

    まず初めに、テンプレートを単純にレンダリングするだけの新しいテンプレートを作ります。これは以前、私たちが投稿リストで使ったテンプレートと同じです。

    <template name="postPage">
      {{> postItem}}
    </template>
    
    client/views/posts/post_page.html

    後でこのテンプレートにはコメントなどの要素を入れていきますが、今のところ {{> postItem}} インクルードのシェルのような役目を果たしています。

    もう一つ指定したルートを作っていきましょう。 今回は、/posts/ の形で URL パスを postPage テンプレートに割り当てます。

    Router.map(function() {
      this.route('postsList', {path: '/'});
    
      this.route('postPage', {
        path: '/posts/:_id'
      });
    });
    
    
    lib/router.js

    この特別な :_id 構文はルーターに2つのことを指定します: 1つ目は、/posts/xyz/ の形のルートをマッチさせます。/posts/xyz/ の “xyz” はどんなものでも問題ありません。 2つ目は、ルーターの params配列で _id プロパティ内の “xyz” 地点を見つけたものはどんなものでも中に入れます。

    ここでは便宜上のために _id だけを使っていることに留意してください。 このルーターはあなたが実際の _id を渡しているのか、ランダムの文字列を渡しているのか判断することはできません。

    私たちは今、正しいテンプレートにルーティングしていますが、まだ何か見逃しています: ルーターは私たちが表示したい投稿の _id を識別しますが、テンプレートはまだ何もわからない状態です。 では、どのようにしてこのギャップを埋めるのでしょうか。

    ありがたいことに、Iron Router には解決策が組み込まれています: Iron Router は、テンプレートのデータコンテキストを指定するのです。 これはテンプレートとレイアウトで作られたおいしいケーキの中をデータコンテキストが満たしていると考えることができます。 簡単に言うと、 テンプレートの中をいっぱいに満たしているものがデータコンテキストです。

    The data context.
    The data context.

    この場合、私たちは URL から得た _id を基にした投稿を探して正しいデータコンテキストを取得しています。

    Router.map(function() {
      this.route('postsList', {path: '/'});
    
      this.route('postPage', {
        path: '/posts/:_id',
        data: function() { return Posts.findOne(this.params._id); }
      });
    });
    
    
    lib/router.js

    そのため、ユーザーがこのルートにアクセスするたびに、 私たちは適切な投稿を見つけて、その情報をテンプレートに引き渡します。 findOne がクエリにマッチした1つの投稿を返して、引数としての id を {_id: id} と略した表現であることを覚えておきましょう。

    ルートの data関数内で、this は現在マッチしているルートに対応しています。 私たちは指定したルート部分にアクセスするために this.params を使うことができます。 (そのことを、私たちは path の中で:を前に置くことで表現しています。)(???)

    More About Data Contexts

    テンプレートのデータコンテキストを設定することで、テンプレートヘルパー内の this の値をコントロールすることができます。

    これは {{#each}} イテレータで非明示的に処理されます。 {{#each}} イテレータは 自動的に現在繰り返し処理されるアイテムそれぞれに繰り返し処理のデータコンテキストを設定します。  

    {{#each widgets}}
      {{> widgetItem}}
    {{/each}}
    

    しかし、{{#with}} を使うことで同じことを明示的に行うことができます。 {{#with}} は単純に「このオブジェクトを取ってきて、次のテンプレートにそれを適用しろ」と示しています。 例えば、このように書くことができます。

    {{#with myWidget}}
      {{> widgetPage}}
    {{/with}}
    

    テンプレートの呼び出しに、コンテキストを引数として引き渡すことで同じ結果を得ることができることがわかります。  そのため先ほどのコードのブロックはこのように書きなおすことができます:

    {{> widgetPage myWidget}}
    

    動的に指定したルートヘルパーを使う

    最終的に、私たちは個別の投稿にリンクをしたい時は常に正しい場所を指し示す必要があります。 再び、私たちは とすることができますが、ルートヘルパーを使った方がより確実です。

    私たちは投稿ルートを postPage と指定したので、{{pathFor ‘postPage’}} ヘルパーを使います。

    <template name="postItem">
      <div class="post">
        <div class="post-content">
          <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
        </div>
        <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
      </div>
    </template>
    
    client/views/posts/post_item.html

    でも待ってください。 /posts/xyz の中の xyz 部分をどこでルーターが取得するのかどれだけ正確に判断するのでしょうか。 結局、私たちはルーターに一つも _id を引き渡していません。

    結局のところ、Iron Router は自分自身で id を判断できるほどスマートだということです。 Iron Router に postPage ルートを使うように指示すると、 Iron Router はルートがどのid を要求するのか識別します。(こういった理由で私たちはpath を定義しました。)

    そのため、ルーターは最もロジカルな場所で利用できる _id を探します: {{pathFor 'postPage’}} ヘルパーのデータコンテキスト、つまりは this です。 図らずも this は個別の投稿に対応するので、(驚くことに!) _id プロパティを内包しています。

    この代わりに、ヘルパーに2つの引数を渡すことで(つまり、{{pathFor 'postPage’ someOtherPost}}として)、 ルーターがどこで _id プ ロパティを探したいかを明示的に指定することもできます。 たとえば、このパターンの実用的な使い方は投稿リストの前や次のリンクを取得することでしょう。

    正しく動作しているか確かめるために、  投稿リストをブラウザで開いて、'Discuss’ ボタンをクリックしましょう。  きっとこのように表示されるはずです。

    A single post page.
    A single post page.

    HTML5 pushState

    もう一つ理解することは、URL の変化が HTML5 の pushState を使うことで起こっていということです。

    Iron Router はアプリの状態に必要な変化をさせるだけでなく、 サイト内の URL クリックを取得して、 ブラウザーがアプリから(browsing away ?)することを防ぎます。

    もしすべてが正しく動いているなら、ページは瞬時に変化します。 実際には、変化するのが早過ぎるため、ある種のページ移行が必要になります。 この点はこの章では範囲外ですが、興味深いトピックです。