目次


MEAN をマスターする

MEAN と Meetup.com、Microdata との融合

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: MEAN をマスターする

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:MEAN をマスターする

このシリーズの続きに乞うご期待。

この連載ではこれまでのところ、UGLI (User Group List and Information) MEAN アプリケーションを単独でカスタマイズしてきました。今回は、Web サイトを他のサイトとうまく連動させることができるかどうかを確かめます。前回のチュートリアルではアプリケーションの中でローカル・データを処理しましたが、今回は今後のミーティングに関する情報を表示するために、MEAN アプリケーションに Meetup.com API を統合します。JSON データは Meetup のものですが、ページの外観と動作はすべて開発者の裁量で決定できます。また、今回は検索エンジンで Web ページがより扱いやすくなるように、Microdata を組み込む方法も学びます。完全なサンプル・コードを入手するには、「ダウンロード」を参照してください。

まずは、UGLI ホーム・ページに今後のイベントに関する情報を Meetup.com から追加する作業に取り掛かります。

HTML と Microdata

Meetup.com の HTML5 Denver Users Group にアクセスすると、図 1 のような Web ページで迎えられます。

図 1. 次回の HTML5 Denver User Group ミーティングに関する Meetup.com の情報
次回の HTML5 Denver User Group ミーティングに関する Meetup.com の情報を示す画面のスクリーンショット
次回の HTML5 Denver User Group ミーティングに関する Meetup.com の情報を示す画面のスクリーンショット

ご覧のように、UGLI ホーム・ページを今後のイベントに関する情報 (次回の講演のタイトル、ミーティングの時刻と場所など) でさらにカスタマイズするために必要な生の素材はすべて Meetup.com ページに揃っています。しかし、Meetup.com ページのデザインは、表示されているデータと緊密に結び付いています。リスト 1 に示すように、このページから抽出して MEAN アプリケーションで再利用したい情報は多数の HTML 要素で雑然としています (以下の HTML は、編集して明確かつ簡潔にしてあります)。

リスト 1. Meetup.com の HTML ソース
<ul>
    <li itemscope="" itemtype="http://data-vocabulary.org/Event">

        <span itemprop="eventType" 
              style="display:none;">Meetup</span>
        <h3>
            <a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/" itemprop="url">
                <span itemprop="summary">"Developing Offline Applications" and "HTML 5 Animations"</span>
            </a>
        </h3>

        <!-- snip -->
    </li>
</ul>

イベントのタイトル「“Developing Offline Applications” and “HTML 5 Animations”」は、HTML 内で何階層か深くネストされたところにあります。HTML 文書 (そして使用している Web ブラウザー) に関する限り、この任意の文字列は順序なしリスト (<ul>) 内にネストされた複数のリスト項目 (<li>) のうちの 1 つに過ぎません。リスト項目には第 3 レベルの見出し (<h3>) が設定されているため、文書階層には既に第 2 レベルと第 1 レベルの見出しが定義されていることが推測できます。見出し内にはハイパーリンク (<a href>) があり、ハイパーリンク内にはテキストを含む任意の <span> があります。

この情報を Meetup.com と同じように表示するには、この HTML マークアップのすべてが必要ですが、私たちのアプリケーションで行う作業は、Meetup.com とはまったく異なる方法で情報を表示することです。それには、データをその表示から切り離す方法を見つけなければなりません。

リスト 1 には、2 つの異なるセマンティクスの層があります。最も基本的な層は、たった今説明した「これはリスト項目で、これはハイパーリンクである」といった「文書」のセマンティクスです。もう 1 つの層は、eventTypeurlsummary などのキーワードで示された「イベント」のセマンティクスです。これらのキーワードは、文書の表示方法とは無関係です。検索エンジン (そして HTML ソースを表示する、知りたがりの人間) にとって、キーワードは情報の「概要」に関するヒントです。これらの属性は、HTML Microdata 仕様の一部となっています (Microdata とそのメタデータの前身について詳しくは、囲み記事「Microformats、Microdata、それに RDFa もあります!」を参照してください)。

この特定のリスト項目に含まれるすべての内容が、ある event に関する情報です。この事実を検索エンジンが把握できる理由は、このページの設計者がリスト項目に itemtype="http://data-vocabulary.org/Event" を追加したからです。このページには任意のハイパーリンクが多数含まれていますが、itemprop="url" が指定されているハイパーリンクは event そのものへのリンクです。eventTypeMeetup です。これは任意のストリングですが、Meetup.com はその Web ページのすべてで一貫してこのストリングを使用します。イベントの summary は、itemprop="summary" 属性が指定された <span> によって識別されます。

<span> 要素は、ブラウザーがページをレンダリングする際に無視する数少ない HTML 要素のうちの 1 つです。<b> 要素内のテキストは太字でレンダリングされ、<h1> のテキストは <h2> のテキストより大きいフォント・サイズで表示され、<a> のテキストは、通常は下線付きの青色のテキストであり、クリック可能です。当然のことながら、これらのデフォルトのスタイル・ルールのすべてをそのまま適用するのではなく、CSS で指定したスタイルを適用することが可能です。ただし、<span> タグは、独自の CSS スタイル設定を追加するためだけに存在しています。つまり、リスト 1 に記載した Meetup.com の HTML スニペットの場合で言うと、「“Developing Offline Applications” and “HTML 5 Animations”」というストリングを、itemprop="summary" というセマンティック・ラベルでラップするためだけに存在します。

イベントの詳細を説明するために MEAN マークアップに追加できるメタデータ項目の完全なリストについては、http://data-vocabulary.org/Event にアクセスしてください。Meetup.com はそもそもこの URL を使用して、イベントを定義しました。

プレースホルダー・イベントをホーム・ページに追加する

ここまでで Meetup.com でのイベント情報の表示方法を大まかに把握できたので、今度は UGLI アプリケーションで同じようにイベント情報を表示する作業に取り掛かります。

テスト・アプリケーションのルートで「mongod」と入力して MongoDB を起動した後、「grunt」と入力して Web アプリケーションを起動します。Web ブラウザーで http://localhost:3000 にアクセスすると、前回のチュートリアルでカスタマイズしたホーム・ページが表示されるはずです (図 2 を参照)。

図 2. UGLI ホーム・ページ
UGLI ホーム・ページのスクリーンショット
UGLI ホーム・ページのスクリーンショット

これから、このホーム・ページに HTML5 Denver User Group の次回のイベントを追加します。この作業は、チュートリアルの残りのすべてにわたって繰り返し行うことになります。まず始めに、ページの基本的な外観のワイヤーフレームを作成するために、静的なプレースホルダー・データを追加します。

テキスト・エディターで public/modules/core/views/home.client.view.html を開きます。バナーの下に、イベントの新規マークアップを追加します (リスト 2 を参照)。

リスト 2. public/modules/core/views/home.client.view.html
<section data-ng-controller="HomeController">
  <div class="jumbotron text-center">
    <!-- snip -->
  </div>

  <div class="row">
    <div>Monday, September 22, 2014</div>
    <h3><a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/">
    "Developing Offline Applications" and "HTML 5 Animations"</a></h3>
    <div class="col-md-4">
     <h4>When</h4>
     <p>6pm</p>
     <h4>Where</h4>
     <address>
       <span>Rally Software</span><br>
       1550 Wynkoop<br>     
       Denver, CO<br>
     </address>
    </div>

    <div class="col-md-8">
     <p><b>6 pm : "Developing Offline Applications with HTML 5" by Venkat Subramaniam</b></p> 
     <p><b>7 pm: Dinner and Networking</b></p> 
     <p><b>7:30 pm: "HTML 5 Animations - building true richness on the web" by Venkat Subramaniam</b></p>
    </div>
  </div>
</section>

上記の更新を行った後のホーム・ページは、ブラウザーでは図 3 のように表示されます。

図 3. UGLI ホーム・ページのワイヤーフレーム
UGLI ホーム・ページのワイヤーフレームのスクリーンショット
UGLI ホーム・ページのワイヤーフレームのスクリーンショット

基本的な HTML 構造が用意できたので、次は、デフォルトのスタイル設定を提供した場合よりも見栄えの良いページにします。そのためには、セマンティクスを追加する必要があります。

Bootstrap には、構造に関するデフォルト HTML 要素 (<div><h3> など) の組み込みスタイルがあります。また、デフォルト HTML 要素にはない、rowcol といった構造に関するアドオン・タイプのクラスも用意されています。

新米の Web 開発者は、表示される情報の観点ではなく、Web ページの構造の観点で考えがちであることから、結果的に big-red-italicleft-column-header などといった名前のカスタム CSS クラスを作成しがちです。このやり方が構文的に誤っているわけではありませんが、Web サイトを長期にわたって保守する場合には、event-dateevent-location といったセマンティックな名前を使用したほうが遥かに簡単だと思います。こうすれば、顧客が 1 年後にすべての小計を赤ではなく緑にするように求めてきた場合、「どのように」表示するか (green-body-text) ではなく、「何」を表示するか (subtotals) を識別する CSS クラスを扱うことができます。また、ページ上でたまたま green-body-text という CSS ルールを使用している他の要素を誤って変更してしまう可能性も少なくなります。

作成した HTML に戻り、セマンティクスの面で適切な CSS クラス (eventevent-dateevent-title など) を追加します。

<div class="row center-block event">
<div class="event-date">Monday, September 22, 2014</div>
<h3 class="event-title"><a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/">
    "Developing Offline Applications" and "HTML 5 Animations"</a></h3>

構造に関する center-block クラスは、前のチュートリアルで学んだ Bootstrap ライブラリーに提供されているものです。この後すぐにわかるように、center-block では、event クラスの width を 75 パーセントに縮小すると、左右で均等にバランスをとった余白が確保されます。

同じ 1 つの要素に構造に関するクラスとセマンティクスに関するクラスを混在させても、まったく問題はありません。実際、長い目で見るとそのほうがアプリケーション固有のクラス (event-*) と汎用クラス (rowcenter-block) を素早く見分けやすくなります。

HTML にクラスを追加したら、次はカスタム CSS ルールを定義します。テキスト・エディターで public/modules/core/css/core.css を開いてください。各モジュールには、それぞれに固有の CSS があるため、その「コンポーネント指向」の考え方にそのまま従うことができます。また、サブディレクトリー・ツリー全体を取り込むほうが、プロジェクト間でモジュールを共有しやすくなります。

リスト 3 に記載する CSS スタイル・ルールを追加します。

リスト 3. public/modules/core/css/core.css
.event {
    width: 75%;
}

.event-date {
    font-style: italic;
}

.event-title {
    margin-top: 0;
}

図 4 に示されているように、ホーム・ページの外観が前よりも若干整えられて、少し洗練されてきました。

図 4. CSS スタイルが適用された UGLI ホーム・ページ
CSS スタイルが適用された UGLI ホーム・ページのスクリーンショット
CSS スタイルが適用された UGLI ホーム・ページのスクリーンショット

最後に、もう一度 HTML ファイルに戻って、Microdata メタデータを追加します。カスタム CSS ルールを定義せず、このステップを省略して CSS ルールの中で Microdata の要素を使用できるかと言えば、もちろんそうすることもできます。しかし、私はこの 2 つを分けておくようにしています。結局のところ、それぞれが果たす目的は異なるからです。一方は CSS スタイルを設定する目的、もう一方は検索エンジン最適化 (SEO) の目的を果たします。例えば今から 5 年後に、Microdata を別の新しい仕様にマイグレーションするように要求するユーザー・ストーリーに対応しなければならないとします。このストーリーに対応すると、その副次作用として Web サイトの外観に悪影響が及ぼされるとしたら残念としか言いようがありません。したがって、CSS と Microdata は互いにまったく影響し合わないようにしておくべきです。

テキスト・エディターでもう一度 public/modules/core/views/home.client.view.html を開きます。リスト 4 に記載するように、イベントのセマンティクスに関する新しい Microdata マークアップを追加します (Microdata を使用したイベントのマークアップの好例については、「Rich snippets - Events」を参照してください)。

リスト 4. Microdata を追加する
<div class="row center-block event" 
   itemscope 
   itemtype="http://data-vocabulary.org/Event">
  <span itemprop="eventType" 
      style="display:none;">Meetup</span> 
  <time class="event-date" 
      itemprop="startDate" 
      datetime="2014-09-22T18:00-06:00">Monday, September 22, 2014</time>
  <h3 class="event-title"><a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/" 
    itemprop="url"><span itemprop="summary">"Developing Offline Applications" and 
    "HTML 5 Animations"</span></a></h3>
  <div class="col-md-4">
    <h4>When</h4>
    <p>6pm</p>
    <h4>Where</h4>
    <address itemprop="location" 
         itemscope 
         itemtype="http://data-vocabulary.org/?Organization">
      <span itemprop="name">Rally Software</span><br>
      <span itemprop="address" 
          itemscope 
          itemtype="http://data-vocabulary.org/Address">
        <span itemprop="street-address">1550 Wynkoop</span><br>  
        <span itemprop="locality">Denver</span>, <span itemprop="region">CO</span><br>
      </span>
    </address>
  </div>

  <div class="col-md-8" itemprop="description">
    <p><b>6 pm : "Developing Offline Applications with HTML 5" by Venkat Subramaniam</b></p> 
    <p><b>7 pm: Dinner and Networking</b></p> 
    <p><b>7:30 pm: "HTML 5 Animations - building true richness on the web" by Venkat Subramaniam</b></p>
  </div>
</div>

作業を開始する前のブラウザーでの表示と最終的にまったく同じ外観になるようにするには、かなりの作業を要するように思えるのではないでしょうか?しかし、その先には、手間をかけて追加したすべてのセマンティック・データのおかげで、自分で作成した Web サイトが検索結果のトップに躍り出たときの満足感が待っています。

リスト 4 を行ごとに説明する代わりに、興味深い点をいくつか取り上げて指摘します。

リスト 1 に記載した Meetup.com の例と同じく、(厳密に SEO だけを目的としているため) 表示されない要素に eventType を追加します。

<span itemprop="eventType" 
      style="display:none;">Meetup</span>

次に、人間とマシンの両方が認識できるように、イベントに日付を追加します。

<time class="event-date" 
      itemprop="startDate" 
      datetime="2014-09-22T18:00-06:00">Monday, September 22, 2014</time>

人間は、「Monday, September 22, 2014」というストリングを即座に解析して、それが日付であることを認識することができます。また、9/22/2014 と 2014-09-22 が同じ日付であることも認識できます。しかし、コンピューターは想像力に欠けるため、少しフォーマット設定を変更して、大きな頭痛の種を和らげられるようにしなければなりません。この例では、以下の変更を行って曖昧さを解消しています。

  • HTML 要素の汎用 <div> を、より具体的な <time> (新しい HTML5 要素) にアップグレードしています。
  • CSS の event-date クラスで、データの外観ではなく内容を識別しています。
  • itemprop="startDate" という Microdata 属性で、この日付を eventstartDate として識別しています。
  • (HTML5 の <time> 要素に含まれる) datetime 属性により、ISO 8601 フォーマットで時刻を一義的に記述しています。このようにすると、マシンが認識できる時刻になると同時に、表示用に綺麗にフォーマット設定されて人間が認識できる時刻表示にもなります。

HTML に対する最大の変更にはイベントのアドレスが関与します。リスト 5 に記載するように、Event スキーマ内には新しいスキーマ (OrganizationAddress) がネストされています。

リスト 5. 組織およびアドレス用の Microdata を追加する
<h4>Where</h4>
<address itemprop="location" 
         itemscope 
         itemtype="http://data-vocabulary.org/?Organization">
    <span itemprop="name">Rally Software</span><br>
    <span itemprop="address" 
          itemscope 
          itemtype="http://data-vocabulary.org/Address">
        <span itemprop="street-address">1550 Wynkoop</span><br>  
        <span itemprop="locality">Denver</span>, <span itemprop="region">CO</span><br>
    </span>
</address>

ブラウザーを Microdata 対応にする

ここまでで Web サイトに Microdata が追加されましたが、正しく追加されたことを確認するにはどうすればよいでしょう?最も簡単な手法は、ブラウザー用の Microdata 拡張機能を使用することです。世間に出回っている Microdata 拡張機能のうち、私が Chrome で好んで使用しているのは Semantic inspector です。

Semantic inspector をインストールしても、通常は、Microdata を使用している Web サイトに出くわすまでは、このブラウザー拡張機能は表示されません。Semantic inspector は、現在の Web ページで Microdata を検出すると、アドレス・バーに赤い m のアイコンを表示します。この小さいアイコンがいかに頻繁に出現するかには驚かされるはずです。m アイコンは、Google、Time.com、Walmart.com をはじめ、多くの人気がある主流の Web サイトにアクセスすると表示されます。このアイコンをクリックすると、Microdata の詳細が表示されます (図 5 を参照)。

図 5. Semantic inspector による Microdata の表示
Semantic inspector によって表示された Microdata のスクリーンショット
Semantic inspector によって表示された Microdata のスクリーンショット

基本的な HTML ワイヤーフレームに Microdata の属性で注釈を付けたので、次は、このワイヤーフレームにインターネットから真新しい JSON を取り込みます。そのために、コントローラー、ビュー、モデル、サービスを格納する新しいイベント・モジュールを作成します。

AngularJS モジュールを作成する

前回のチュートリアルでは Yeoman を使用して、Express ルートと Mongoose モデルを含む完全な CRUD モジュールの土台を作成しました。このユーザー・ストーリーの場合、外部 Web アプリケーションからロー JSON データが提供されるため、サーバー・サイドのインフラストラクチャーは必要ありません。幸い、MeanJS Yeoman ジェネレーターの作成者たちは、このような要件を予測して、アプリケーションのクライアント・サイドの AngularJS 部分に専用の別のジェネレーターを用意してくれています。

yo meanjs:angular-module events」と入力して、events という名前の新規 AngularJS モジュールを作成します。AngularJS モジュールは、アプリケーションでの特定のデータ・タイプに固有のファイルを論理的にグループ化したものです。公式の AngularJS ドキュメントで説明しているように、「モジュールは、アプリケーションのそれぞれ異なる部分 (コントローラー、サービス、フィルター、ディレクティブなど) のコンテナーとみなすことができます」。

要素の選択を求めるプロンプトが出されたら、リストに含まれるすべての要素を選択します (リスト 6 を参照)。

リスト 6. モジュールを生成する
[?] Which folders would you like your module to include? 
 ? config
 ? controllers
 ? css
 ? directives
 ? filters
 ? img
 ? services
 ? tests
 ? views

 create public/modules/events/events.client.module.js

生成された時点では、モジュールは単なる空のディレクトリーのセットに過ぎません。作成するすべての AngularJS モジュールにすべてのディレクトリーを使用するわけではありませんが、いずれモジュールの各構成部分を保管するために、適切な名前が付けられたわかりやすい場所があることがわかっていると安心です。

次のステップでは、生成したこのモジュールに、コントローラーを追加します。

AngularJS コントローラーを作成する

いったんモジュールの土台を作成すれば、コントローラーの土台を作成するのも簡単です。「yo meanjs:angular-controller events」と入力し、プロンプトが出されたら events モジュールを選択します (リスト 7 を参照)。

リスト 7. コントローラーを生成する
[?] Which module does this controller belongs to? 
  articles 
  core 
? events 
  talks 
  users 
  
create public/modules/events/controllers/events.client.controller.js
create public/modules/events/tests/events.client.controller.test.js

上記の出力を見るとわかるように、Yeoman ジェネレーターはコントローラーを controllers ディレクトリーに配置し、関連するテストを指定のモジュールの tests ディレクトリーに配置します。

この時点で、「なぜコントローラーを作成し、なぜこのコントローラーを扱う必要があるのか?」という疑問を抱くのも当然です。AngularJS は、クライアント・サイドのモデル・ビュー・コントローラー (MVC) フレームワークであることを思い出してください。最終的には、ビュー (このチュートリアルで前に作成した HTML の <div>) にモデル・データ (Meetup.com からのイベント・データが格納された JSON 構造) が取り込まれることになります。ビューがこのモデルにアクセスできるようにするには、コントローラーがそのジョブとして、すべてを 1 つに統合して、ビューに必要なモデル・データを提供しなければなりません。

以下の簡単な例で MVC の各構成部分がどのように組み合わさるのかを明らかにします。まず、テキスト・エディターで public/modules/events/controllers/events.client.controller.js (リスト 8 を参照) を開いてください。

リスト 8. スタブアウトされた空の AngularJS コントローラー
'use strict';

angular.module('events').controller('EventsController', ['$scope',
  function($scope) {
    // Events controller logic
    // ...
  }
]);

この後すぐに、このコントローラーを特定の DOM 要素にバインドします。モデルをビューに渡す重要な役割を果たすのは、$scope 変数です。

$scopetitle 変数 (モデル) を追加します (リスト 9 を参照)。

リスト 9. $scope 変数を AngularJS コントローラーに追加する
'use strict';

angular.module('events').controller('EventsController', ['$scope',
  function($scope) {
    $scope.title = 'High Performance WebSocket';

  }
]);

次に public/modules/core/views/home.client.view.html で、EventsControllerEvent DOM 要素 (ビュー) に追加します。

<div class="row center-block event" 
       itemscope 
       itemtype="http://data-vocabulary.org/Event"
       ng-controller="EventsController">

お察しの通り、上記のコードによって、コントローラーが DOM 要素にバインドされます。$scope 変数は、この <div> とその子要素にのみ有効です。コントローラーは、さまざまな DOM 要素に必要なだけバインドすることができます。各要素が、新規コントローラーの独自のインスタンスとその独自の $scope を取得します。

続いて、ハードコーディングされたテキストの代わりとなる {{title}} プレースホルダーをワイヤーフレーム HTML に追加します。

<h3 class="event-title"><a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/" 
itemprop="url"><span itemprop="summary">{{title}}</span></a></h3>

結果を Web ブラウザーで表示すると、{{title}} プレースホルダーが EventsController で指定したテキストで置き換えられていることがわかるはずです (図 6 を参照)。

図 6. 実際のテキストで置き換えられたプレースホルダー
実際のテキストで置き換えられたプレースホルダーのスクリーンショット
実際のテキストで置き換えられたプレースホルダーのスクリーンショット

単純ながらも機能するサンプル・コードが用意できました。次は、このコードを拡張する作業に移ります (つまり、アプリケーションが機能するようになったので、再びアプリケーションを分解します)。$scope には好きなだけ変数を追加することができます。追加する変数は、単純な単一の値でも、完全な JSON オブジェクトでも構いません。

Meetup.com に対する HTTP リクエストを行って、次回のイベントに関する JSON を取得する方法がわかるまではもうすぐです。それまでは、スタブアウトされた $scope と、実際の Ajax 呼び出しによって取得される内容を反映した簡単なモック・データを使用します (リスト 10 を参照)。

リスト 10. JSON レスポンスのモック
'use strict';

angular.module('events').controller('EventsController', ['$scope',
  function($scope) {
        $scope.title = 'High Performance WebSocket';
        $scope.event = {
          'name': '"Developing Offline Applications" and "HTML 5 Animations"',
          'time': 1411430400000,
          'event_url': 'http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/',
          'description': '<p><b>6 pm : "Developing Offline 
          Applications with HTML 5" by Venkat Subramaniam</b></p>',
          'venue': {
            'name': 'Rally Software',
            'address_1': '1550 Wynkoop',
            'city': 'Denver',
            'state': 'CO',
          }      
       }
    }
]);

ご覧のように、$scope.event 変数には複雑にネストされた JSON オブジェクトが格納されます。この新しいモデル・データを利用するようにビューを編集してください (リスト 11 を参照)

リスト 11. さらに他のプレースホルダーを HTML に追加する
<h3 class="event-title"><a href="{{event.event_url}}" itemprop="url"><span 
itemprop="summary">{{event.name}}</span></a></h3>
<div class="col-md-4">
  <h4>When</h4>
  <p>{{event.time}}</p>
  <h4>Where</h4>
  <address itemprop="location" 
           itemscope 
           itemtype="http://data-vocabulary.org/Organization">
    <span itemprop="name">{{event.venue.name}}</span><br>
    <span itemprop="address" 
          itemscope 
          itemtype="http://data-vocabulary.org/Address">
      <span itemprop="street-address">{{event.venue.address_1}}</span><br>   
      <span itemprop="locality">{{event.venue.city}}</span>, 
      <span itemprop="region">{{event.venue.state}}</span><br>
    </span>
  </address>
</div>

<div class="col-md-8" itemprop="description">
  {{event.description}}
</div>

結果を Web ブラウザーで表示すると、テンプレート・ビューにプレースホルダーを追加したすべての箇所に、$scope.event の値が表示されているはずです (図 7 を参照)。

図 7. JSON のモック・データが取り込まれた UGLI ホーム・ページ
JSON のモック・データが取り込まれた UGLI ホーム・ページのスクリーンショット
JSON のモック・データが取り込まれた UGLI ホーム・ページのスクリーンショット

実際のライブ・データを取り込む AngularJS サービスを作成する前に、簡単なビュー関連の処理をいくつか完了する必要があります。具体的には、日付をフォーマット設定するための AngularJS フィルターをいくつか追加し、{{event.description}} プレースホルダーに、エスケープされた HTML のロー・コードではなく、レンダリングされた HTML を表示するという処理です。

AngularJS フィルターを追加する

(カメラや Instagram フィルターとほとんど同様の) AngularJS フィルターを使用すると、データがどのように表示されるかが変わります。フィルターをビューに追加する理由は、コンテンツ自体を変更するのではなく、モデル・データの表示を変更するためです。

テンプレートのプレースホルダーにフィルターを適用するには、データ要素の後にパイプ (|) とフィルター名を追加します (例えば、{{product_code | uppercase}})。AngularJS には、uppercaselowercasecurrencynumber を含む複数の組み込みフィルターが用意されています。また、独自のカスタム・フィルターを作成することもできます。

私が常に使用する結果となるフィルターの 1 つは、date フィルターです。このフィルターを使用すると、カスタム・パターンを使用して、日付値の表示のフォーマットを設定することができます。

例えば以下のようにして、前に作成した time 要素に date フィルターを適用します。

<time class="event-date" 
  itemprop="startDate" 
  datetime="{{event.time | date:'yyyy-MM-ddTHH:mm:ss:Z'}}">{{event.time | 
  date:'EEEE, MMMM, d, yyyy'}}</time>

上記では、同じ event.time フィールドに 2 つの異なるフィルターを適用していることに注意してください。EEEE というコードは、略さず完全に記述した曜日を (例えば、「Monday」と) 表示します。EEE というコードは、曜日を「Mon」までで切り捨てて表示し、EE は「Mo」と、E は「M」と表示します。M というコードは、月の名前に対して同じように機能します。d というコードは日、y というコードは年を対象としています。

event.time フィールドがホーム・ページに表示されるのは 1 回だけではありません。以下のコードで「When」の外観を変更して、時刻と AM/PM サフィックスが表示されるようにします。

<h4>When</h4>
<p>{{event.time | date:'h a'}}</p>

MEAN アプリケーションで AngularJS フィルターを多用することで、MVC デザイン・パターンの重要な教義の 1 つが確実に適用されます。その教義とは、「データ・モデルは、ビューに関係する事項とは切り離す必要がある」というものです。

event.time が適切にフォーマット設定されていれば、event.description の外観を修正するだけで済みます。この修正をするには、AngularJS に対し、このフィールドにはエスケープされていない HTML を表示しても安全であることを知らせる必要があります。

エスケープされていない HTML を表示する

これまで扱った JSON データのほぼすべては、純粋なデータでした。つまり、ネストされた HTML 要素がまったくないデータということです。その例外は、event.description フィールドだけです。

(信用できるソースであるかどうかは別として) 外部ソースから HTML を受信する場合は、常に潜在的なセキュリティー・リスクが付きまといます。含まれている HTML によって不要な JavaScript ライブラリーが取り込まれ、それらのライブラリーによってデータが他の Web サイトにさらされる可能性があります。

こうしたリスクを回避するために、AngularJS はテンプレート化されたデータを自動的にサニタイジングします。その方法とは、検出した HTML 要素のすべてをエスケープし、山括弧は対応する &gt;&lt; に置き換えるというものです。この動作は前のセクションで説明したような明示的なフィルターではありませんが、概念は同様です。

event.description フィールドの場合には、ローカル HTML と一緒に外部 HTML を表示しても安全であることを AngularJS に伝える必要があります。それには、テンプレートから {{event.description}} プレースホルダーを削除し、代わりに ng-bind-html 属性を追加することによってテンプレートを調整します。

<div class="col-md-8" itemprop="description" ng-bind-html="event.description"></div>

ブラウザーでホーム・ページを表示すると、エスケープされて表示されていた HTML 要素 <b><p> が消えていて、代わりにレンダリングされたテキストが表示されているはずです。

これで、コントローラー、モデル、ビューが揃いました。後は、最後に残されたステップとして、コントローラーに含まれる JSON のモック・コンテンツを、Ajax リクエストから返されるライブ・データに置き換えるだけです。このステップを実行するために、もう 1 つの要素をモジュールに追加します。それは、サービスです。

サービスを作成する

AngularJS サービスは、とりわけ Ajax リクエストを実行するために使用されます。外部 Meetup.com API とやり取りするには、このサービスが完璧なソリューションとなります。

コントローラーから直接 Ajax リクエストを実行したいという気になるかもしれませんが、それは近視眼的です。イベント・データが他のコントローラーで必要になったらどうしますか?コントローラーのソース・コードをコピーして別のコントローラーに貼り付けようなどとは間違いなく思わないですよね?共通のデータを複数のコントローラーで容易に共有できるようにするには、代わりにサービスを作成します。

yo meanjs:angular-service events」と入力して、events サービスを作成します (リスト 12 を参照)。モジュールの選択を求めるプロンプトが出されたら、events モジュールを選択します。

リスト 12. AngularJS サービスを作成する
$ yo meanjs:angular-service events
[?] Which module does this service belongs to? 
  articles 
  core 
events 

  talks 
  users 

create public/modules/events/services/events.client.service.js

AngularJS には、HTTP/Ajax リクエストを実行するための $http というプリビルドされたサービスが用意されています (すべての AngularJS サービスには、プレフィックスとして $ が付いています)。$http を使用する方法は、これをサービスに注入することです。events サービスが機能するようになったら、このサービスを EventsController に注入します (AngularJS は、至るところで依存関係の注入を使用します)。

EventsController で使用している $scope オブジェクトを覚えていますか? $scope は、コントローラーに注入されるサービスです。リスト 13 に示されているように、$scope サービスを注入するには、このサービスを宣言した上で引数として関数に渡します。

リスト 13. $scope サービスを注入する
'use strict';

angular.module('events').controller('EventsController', ['$scope',
  function($scope) {
    $scope.title = 'High Performance WebSocket';
  }
]);

サービス名を 2 回入力するのは無駄に冗長であるように思えるかもしれませんが、こうすると、MEAN アプリケーションを本番用に準備する際の縮小化と連結のプロセスが容易になります。

サービスを注入する方法がわかったので、今度はその知識を実際に役立てます。テキスト・エディターで public/modules/events/services/events.client.service.js (リスト 14 を参照) を開きます。

リスト 14. public/modules/events/services/events.client.service.js
'use strict';

angular.module('events').factory('Events', [
    function() {
        // Events service logic

        // ...

        // Public API
        return {
            someMethod: function() {
                return true;
            }
        };
    }
]);

$http サービスを注入します (リスト 15 を参照)。

リスト 15. $http サービスを注入する
angular.module('events').factory('Events', ['$http',
    function($http) {
        // Events service logic
        // ...

        // Public API
        return {
            someMethod: function() {
                return true;
            }
        };
    }
]);

次に、someMethodgetNextEvent に変更し、基本的な機能をスタブアウトします (リスト 16 を参照)。

リスト 16. Ajax 呼び出しから JSON を返す
'use strict';

angular.module('events').factory('Events', ['$http',
  function($http) {
    // Public API
    return {
      getNextEvent: function() {
        var url = 'http://api.meetup.com/2/events?status=upcoming&order=
        time&limited_events=False&group_urlname=HTML5-Denver-Users-Group&desc=
        false&offset=0&photo-host=public&format=json&page=1&fields=
        &sig_id=13848777&sig=7aa5d53f450ee5449945e8ee89b8cba8968d9e30&callback=JSON_CALLBACK';

        var request = $http.jsonp(url);
        return request;
      }
    };
  }
]);

上記に示されている (比較的冗長な) URL は、HTML5 Denver User Group の次回のイベントを返します (Meetup.com では、その API をいろいろと操作できるように、便利なサンドボックスを提供しています)。この URL をブラウザーにコピーすると、完全な JSON レスポンスが返されます。リスト 17 に記載するレスポンスは、明確にするために編集してあります。

リスト 17. Meetup.com の API から返される JSON レスポンス
{
  "results": [
    {
      "status": "upcoming",
      "visibility": "public",
      "venue": {
        "id": 21506832,
        "name": "Rally Software",
        "state": "CO",
        "address_1": "1550 Wynkoop",
        "city": "Denver"
      },
      "id": "160326502",
      "time": 1411430400000,
      "event_url": "http:\/\/www.meetup.com\/HTML5-Denver-Users-Group\/events\/160326502\/",
      "description": "<p><b>6 pm : \"Developing Offline Applications with HTML 5\" 
      by Venkat Subramaniam<\/b><\/p> ",
      "name": "\"Developing Offline Applications\" and \"HTML 5 Animations\""
    }
  ],
  "meta": {
    "count": 1,
    "total_count": 3,
    "next": "http:\/\/api.meetup.com\/2\/events?status=upcoming&sig_id=13848777&
    order=time&limited_events=False&group_urlname=HTML5-Denver-Users-Group&
    desc=false&sig=7aa5d53f450ee5449945e8ee89b8cba8968d9e30&photo-host=public&offset=1&
    format=json&page=1&fields="
  }
}

results 配列に含まれる JSON オブジェクトは、お馴染みのことでしょう。前に、同様のデータを EventsController にスタブアウトしました。ただし、この完全な JSON レスポンスには、データをホーム・ページにレンダリングする際に必要にはならない他の情報 (meta など) も含まれていることに注意してください。幸い、JSON レスポンスを変換してから渡すことは可能です。それには、変換ロジックを events サービスに追加します (リスト 18 を参照)。

リスト 18. JSON レスポンスを変換する
    // Public API
    return {
      getNextEvent: function() {
        var url = 'http://api.meetup.com/2/events?status=upcoming&order=
        time&limited_events=False&group_urlname=HTML5-Denver-Users-Group&desc=
        false&offset=0&photo-host=public&format=json&page=1&fields=
        &sig_id=13848777&sig=7aa5d53f450ee5449945e8ee89b8cba8968d9e30&callback=JSON_CALLBACK';
        
        var returnFirstElement = function (data, headers) {
                    return data.results[0];
                };

        var request = $http.jsonp(url, {transformResponse: returnFirstElement});
        return request;
      }
    };
  }

]);

この変換ロジックにより、JSON には results 配列の最初の要素だけが格納されることになります。その他の余計な JSON 情報はすべて破棄されます。

開発プロセスで役立つように、success ハンドラーおよび error ハンドラーを追加します (リスト 19 を参照)。以下のコードによって、レスポンス・データがコンソールに記録されます。このコードはカスタマイズするのも、完全に無視するのも自由です。

リスト 19. success ハンドラーおよび error ハンドラーを追加する
    // Public API
    return {
      getNextEvent: function() {
        var url = 'http://api.meetup.com/2/events?status=upcoming&order=
        time&limited_events=False&group_urlname=HTML5-Denver-Users-Group&desc=
        false&offset=0&photo-host=public&format=json&page=1&fields=
        &sig_id=13848777&sig=7aa5d53f450ee5449945e8ee89b8cba8968d9e30&callback=JSON_CALLBACK';

        
        var returnFirstElement = function (data, headers) {
                    return data.results[0];
                };

        var request = $http.jsonp(url, {transformResponse: returnFirstElement});
        
        request.success(function(data, status, headers, config) {
            console.log('SUCCESS');
            console.log(data);
        });
        request.error(function(data, status, headers, config) {
            console.log('ERROR');
            console.log(data);
        });

        return request;
      }
    };
  }
]);

events サービスはこれで完成です。次は、このサービスを EventsController に注入します。EventsController をリスト 20 に示すように変更してください。

リスト 20. events サービスを EventsController に注入する
'use strict';

angular.module('events').controller('EventsController', ['$scope', 'Events',
  function($scope, Events) {
        $scope.event = undefined;

        Events.getNextEvent().success(function(data){
          $scope.event = data;          
        });
    }
]);

すべてが期待通りに運んでいれば、ホーム・ページに完全なイベントの説明が表示されるはずです (図 8 を参照)。前にモックアウトした内容ではなく、講演に関する詳細な説明が表示された場合は、万事順調に機能していることになります。

図 8. 完全に機能している例の実際
完全に機能している例の実際のスクリーンショット
完全に機能している例の実際のスクリーンショット

FOUC が表示されないようにする

ホーム・ページが最初にレンダリングされた時点から Meetup.com に対する Ajax リクエストを実行するまでの間に、嫌な FOUC (Flash of Unstyled Content) が発生したことに気が付きましたか?気付かなかった場合は、ブラウザーを何度か最新の表示に更新すると FOUC に気付くはずです。

FOUC は致命的なバグではありませんが、プロフェッショナルなアプリケーションであるという印象を与えないことは確かです。ありがたいことに、AngularJS の開発者たちは、この一般的な問題に対する単純で簡潔なソリューションを提供しています。

最後に行う変更として、home.client.view.html で ng-show を使用して、モデル・データが揃うまでビューを非表示にしてください。

<div class="row center-block event" 
     itemscope 
     itemtype="http://data-vocabulary.org/Event"
     ng-controller="EventsController"     ng-show="event">

ng-show 属性を <div> に追加すると、$scope.event 変数にデータが取り込まれるまで <div> 全体が非表示になります。Meetup.com 対する Ajax リクエストによって JSON (モデル) が返された時点で、<div> (ビュー) が表示されます。

まとめ

UGLI アプリケーションは、まさに形になってきました。現在、外部 API から JSON データを取得して、結果のビューを Microdata を使用してフォーマット設定するようになっています。これにより、検索エンジンとその他の自動プロセスは、Web ページに表示されて人間が読む情報と同じ情報を利用することができます。

次回のチュートリアルでは、OAuth による許可と認証に取り組みます。それまでは、楽しみながら MEAN をマスターしてください。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development, Open source
ArticleID=994263
ArticleTitle=MEAN をマスターする: MEAN と Meetup.com、Microdata との融合
publish-date=01152015