次の方法で共有


ASP.NET

単一ページ アプリケーション: ASP.NET を使用して最新の応答性の高い Web アプリケーションを構築する

Mike Wasson

コード サンプルのダウンロード

Single Page Application (SPA: 単一ページ アプリケーション) は、単一の HTML ページを読み込んで、ユーザーのアプリケーション操作に合わせてページを動的に更新する Web アプリケーションです。

SPA では AJAX と HTML 5 を使用して、定期的にページを再読み込みしない、滑らかで応答性の高い Web アプリケーションを作成できます。ただし、これは処理の大半をクライアント側で JavaScript を使用して実行することを意味します。従来の ASP.NET 開発者にとって、これは克服が難しいこともある課題です。さいわい、SPA の作成を容易にするオープン ソース JavaScript フレームワークは多数存在します。

この記事では、単純な SPA アプリケーションの作成について説明します。その過程で、モデル - ビュー - コントローラー (MVC: Model-View-Controller) パターン、モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) パターン、データ バインディング、ルーティングなど、SPA を構築するための基本概念をいくつか紹介します。

サンプル アプリケーションについて

今回作成したサンプル アプリケーションは、単純な映画データベースです (図 1 参照)。ページの左端の列にジャンル一覧を表示し、ユーザーがジャンルをクリックしたら、クリックしたジャンルの映画一覧を表示します。エントリの横の [Edit] ボタンをクリックすると、そのエントリを変更できます。編集が完了したら、[Save] をクリックして更新をサーバーに送信することも、[Cancel] をクリックして変更を元に戻すこともできます。


図 1 単一ページ アプリケーションの映画データベース アプリケーション

今回は 2 つのバージョンのアプリケーションを作成しました。1 つは Knockout.js ライブラリを使用したバージョンで、もう 1 つは Ember.js ライブラリを使用したバージョンです。これら 2 つのライブラリは設計思想が異なるため、2 つのバージョンを比較すると役に立ちます。どちらのバージョンでも、クライアント アプリケーションの JavaScript は 150 行未満でした。サーバー側には、クライアントに JSON を提供するために ASP.NET Web API を使用しました。両バージョンのアプリケーションのソース コードは、github.com/MikeWasson/MoviesSPA (英語) で公開しています。

(注: サンプル アプリケーションは、Visual Studio 2013 のリリース候補 (RC) 版を使用して作成しました。Release to Manufacturing (RTM) 版で変更される機能もあるでしょうが、コードへの影響はないでしょう)。

背景

従来の Web アプリケーションでは、アプリケーションからサーバーを呼び出すたびに、サーバーで新しい HTML ページをレンダリングし、ブラウザーのページを更新していました。Web フォーム アプリケーションや PHP アプリケーションを作成したことのある開発者は、このようなページ ライフサイクルに見覚えがあるでしょう。

SPA では、最初のページを読み込んだ後は、サーバーとのすべてのやり取りを AJAX 呼び出しを使用して行います。これらの AJAX 呼び出しは、通常、マークアップ形式ではなく JSON 形式のデータを返します。SPA ではこの JSON データを使用して、ページを再読み込みせずに動的にページを更新します。図 2 に、2 つの設計思想の違いを示します。


図 2 従来のページ ライフサイクルと SPA ライフサイクル

SPA のメリットの 1 つは明白です。つまり、アプリケーションはページの再読み込みと再レンダリングによる厄介な影響を受けないので、動きが滑らかになり応答性も高くなります。1 つ目のメリットほどわかりやすくはありませんが、Web アプリケーションの設計方法に関するメリットもあります。アプリケーション データを JSON として送信するので、プレゼンテーション (HTML マークアップ) とアプリケーション ロジック (AJAX 要求と JSON 応答) が分離します。

このように分離していると、各層の設計や発展が容易になります。優れた設計の SPA では、アプリケーション ロジックを実装するコードに触れずに HTML マークアップを変更できます (少なくとも、これが理想です)。後ほどデータ バインディングについて説明する際に、この手法を実際にお見せします。

純粋な SPA では、すべての UI 操作をクライアント側で JavaScript と CSS を使用して処理します。最初のページの読み込み後、サーバーは完全にサービス層として機能します。クライアントでは、送信が必要な HTTP 要求を把握するだけで十分です。サーバーでバックエンドの処理を実装している方法は、関係ありません。

このアーキテクチャでは、クライアントとサービスは独立しています。サービスを実行しているバックエンド全体を置き換えることができ、API を変更しない限りクライアントが機能しなくなることはありません。逆に、サービス層を変更せずに、クライアント アプリケーション全体を置き換えることもできます。たとえば、同じサービスを使用するネイティブ モバイル クライアントを作成できます。

Visual Studio プロジェクトを作成する

Visual Studio 2013 では、ASP.NET Web アプリケーション プロジェクトの種類は 1 種類です。プロジェクトのウィザードで、作成するプロジェクトに組み込む ASP.NET コンポーネントを選択できます。今回は、空のテンプレートで作成を開始し、[Add folders and core references for] (以下にフォルダーおよびコア参照を追加) の下にある [Web API] チェック ボックスをオンにして、ASP.NET Web API をプロジェクトに追加しました (図 3 参照)。


図 3 Visual Studio 2013 での新しい ASP.NET プロジェクトの作成

新しいプロジェクトには、Web API に必要なすべてのライブラリと、Web API 構成コードが備わっています。今回は Web フォームや ASP.NET MVC へのあらゆる依存関係を削除しました。

Visual Studio 2013 には、単一ページ アプリケーション テンプレートが用意されています (図 3 参照)。このテンプレートを選択すると、Knockout.js で構築されたスケルトン SPA がインストールされます。このテンプレートは、メンバーシップ データベースや外部認証プロバイダーを使用したログインをサポートしています。今回はもっと単純なサンプルをゼロから作成する方法を示すので、このテンプレートは使用しませんでした。ただし、アプリケーションに認証を追加する場合は特に、この SPA テンプレートが役に立ちます。

サービス層を作成する

今回は ASP.NET Web API を使用して、アプリケーション用の単純な REST API を作成しました。ここでは Web API については詳しく説明しません。ASP.NET Web API の詳細については、asp.net/web-api (英語) を参照してください。

まず、このアプリケーションには映画を表す Movie クラスを作成しました。このクラスでは、次の 2 つの処理を行います。

  • Entity Framework (EF) に、映画データを保存するデータベース テーブルの作成方法を指示する。
  • Web API に、JSON ペイロードの形式を指示する。

どちらの処理にも、必ずしも同じモデルを使用する必要はありません。たとえば、データベース スキーマを JSON ペイロードから変更こともできます。このアプリケーションでは、簡単な処理を採用しました。

namespace MoviesSPA.Models
{
  public class Movie
  {
    public int ID { get; set; }
    public string Title { get; set; }
    public int Year { get; set; }
    public string Genre { get; set; }
    public string Rating { get; set; }
  }
}

次に、Visual Studio のスキャフォールディングを使用して、EF を使用する Web API コントローラーをデータ層として作成しました。スキャフォールディングを使用するには、ソリューション エクスプローラーの [Controllers] フォルダーを右クリックし、[Add] (追加) をポイントして [New Scaffolded Item] (新しくスキャフォールディングされたアイテム) をクリックします。Add Scaffold (スキャフォールディングの追加) ウィザードで、[Web API 2 Controller with actions, using Entity Framework] (Entity Framework を使用したアクションがある Web API 2 コントローラー) をクリックします (図 4 参照)。


図 4 Web API コントローラーの追加

図 5 に、Add Controller (コントローラーの追加) ウィザードを示します。今回はコントローラーに MoviesController という名前を付けました。REST API の URI はコントローラーの名前に基づいているため、コントローラー名は重要です。また、EF 6 の新しい非同期機能を利用するために、[Use async controller actions] (非同期コントローラー アクションを使用します) チェック ボックスをオンにしました。モデルには Movie クラスを選択しました。また [New data context] (新しいデータ コンテキスト) をクリックして、新しい EF データ コンテキストを作成しました。


図 5 Add Controller (コントローラーの追加) ウィザード

ウィザードによって、次の 2 つのファイルが追加されます。

  • MoviesController.cs: アプリケーションに REST API を実装する Web API コントローラーを定義します。
  • MovieSPAContext.cs: 基本的には、基盤となるデータベースのクエリ用メソッドを提供する、EF の "接着剤" です。

図 6 に、スキャフォールディングで作成される既定の REST API を示します。

図 6 Web API スキャフォールディングで作成される既定の REST API

HTTP 動詞 URI 説明
GET /api/movies すべての映画の一覧を取得する
GET /api/movies/{id} {id} と一致する ID の映画を取得する
PUT /api/movies/{id} {id} と一致する ID の映画を更新する
POST /api/movies 新しい映画をデータベースに追加する
DELETE /api/movies/{id} 映画をデータベースから削除する

中かっこ内の値は、プレースホルダーです。たとえば、ID が5 の映画を取得する URI は、/api/movies/5 です。

今回は、この API を拡張して、指定したジャンルの映画をすべて取得するメソッドを追加しました。

public class MoviesController : ApiController
{
  public IQueryable<Movie> GetMoviesByGenre(string genre)
  {
    return db.Movies.Where(m =>
      m.Genre.Equals(genre, StringComparison.OrdinalIgnoreCase));
  }
  // Other code not shown

ジャンルは、クライアントから、URI のクエリ文字列の形式で指定します。たとえば、Drama というジャンルの映画をすべて取得するには、クライアントから /api/movies?genre=drama に対する GET 要求を送信します。クエリ パラメーターは、Web API によって GetMoviesByGenre メソッドの genre パラメーターに自動的にバインドされます。

Web クライアントを作成する

ここまでは、REST API を作成しただけです。GET 要求を /api/movies?genre=drama に送信した場合の未処理の HTTP 応答は、次のようになります。

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Date: Tue, 10 Sep 2013 15:20:59 GMT
Content-Length: 240
[{"ID":5,"Title":"Forgotten Doors","Year":2009,"Genre":"Drama","Rating":"R"}, {"ID":6,"Title":"Blue Moon June","Year":1998,"Genre":"Drama","Rating":"PG-13"},{"ID":7,"Title":"The Edge of the Sun","Year":1977,"Genre":"Drama","Rating":"PG-13"}]

ここからは、この応答を使用して実際的な処理を行うクライアント アプリケーションを作成します。基本的なワークフローは次のとおりです。

  • UI で AJAX 要求をトリガーする。
  • HTML を更新して、応答性の高いペイロードを表示する。
  • AJAX エラーを処理する。

これらの処理はすべて手動でコーディングできます。たとえば、映画のタイトル一覧を作成する jQuery コードは、次のようになります。

    $.getJSON(url)
      .done(function (data) {
        // On success, "data" contains a list of movies
        var ul = $("<ul></ul>")
        $.each(data, function (key, item) {
          // Add a list item
          $('<li>', { text: item.Title }).appendTo(ul);
        });
      $('#movies').html(ul);
    });

このコードにはいくつかの問題があります。アプリケーション ロジックとプレゼンテーション ロジックが混在していて、HTML に密接に結び付いています。また、大量のコードを記述する必要もあります。アプリケーションに注力する代わりに、DOM 操作用のイベント ハンドラーとコードを作成することになります。

解決策は、JavaScript フレームワークを基盤として構築することです。さいわい、選択できるオープン ソース JavaScript フレームワークは多数あります。有名なフレームワークをいくつか挙げると、Backbone、Angular、Ember、Knockout、Dojo、JavaScriptMVC などがあります。ほとんどのフレームワークでは MVC パターンや MVVM パターンのバリエーションを使用しているので、これらのパターンを復習すると便利でしょう。

MVC パターンと MVVM パターン

MVC パターンの歴史は 1980 年代の、初期のグラフィカル UI にまでさかのぼります。MVC の目的は、コードを 3 つの役割に分けることでした (図 7 参照)。各役割の内容は、次のとおりです。

  • モデル: ドメイン データとビジネス ロジックを表します。
  • ビュー: モデルを表示します。
  • コントローラー: ユーザー入力を受け取り、モデルを更新します。


図 7 MVC パターン

最近になって現れた MVC のバリエーションが、MVVM パターンです (図 8 参照)。MVVM の役割の内容は、次のとおりです。

  • モデル: やはりドメイン データを表します。
  • ビューモデル: ビューの抽象表現です。
  • ビュー: ビューモデルを表示し、ユーザー入力をビューモデルに送信します。


図 8 MVVM パターン

JavaScript の MVVM フレームワークでは、ビューはマークアップで、ビューモデルはコードです。

MVC にはさまざまなバリエーションがあり、MVC の文献の多くは混乱を招きやすく矛盾しています。Smalltalk-76 で登場した設計パターンを最新の Web アプリケーションで今でも使用していることを考えれば、これは驚くことではないでしょう。したがって、理論を知るのも良いことですが、使用する特定の MVC フレームワークについて理解することが肝心です。

Knockout.js を使用して Web クライアントを構築する

サンプル アプリケーションの 1 つ目のバージョンでは、Knockout.js ライブラリを使用しました。Knockout は、ビューをビューモデルに結び付けるデータ バインディングを使用した、MVVM パターンに準拠しています。

データ バインディングを作成するには、HTML 要素に特別なデータ バインディング属性を追加します。たとえば、次のようなマークアップを使用すると、span 要素がビューモデルの genre というプロパティにバインドされます。genre の値が変わるたびに、Knockout によって自動的に HTML が更新されます。

<h1><span data-bind="text: genre"></span></h1>

バインディングは逆方向にも機能します。たとえば、ユーザーがテキスト ボックスにテキストを入力すると、Knockout によってビューモデルの対応するプロパティが更新されます。

すばらしいのは、データ バインディングが宣言型である点です。ビューモデルを HTML ページ要素に結び付ける必要はありません。データ バインディング属性を追加するだけで、Knockout によって残りの処理が行われます。

図 9 に示すように、今回はまず、データ バインディングを使用せずに標準的なレイアウトの HTML ページを作成しました。

(注: 今回はアプリケーションのスタイル設定に Bootstrap ライブラリを使用しました。したがって、実際のアプリケーションには、書式を制御するための <div> 要素と CSS クラスが他にも多数存在します。ここでは、コード例をわかりやすくするために省略しています)。

図 9 初期の HTML レイアウト

<!DOCTYPE html>
<html>
<head>
  <title>Movies SPA</title>
</head>
<body>
  <ul>
    <li><a href="#"><!-- Genre --></a></li>
  </ul>
  <table>
    <thead>
      <tr><th>Title</th><th>Year</th><th>Rating</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><!-- Title --></td>
        <td><!-- Year --></td>
        <td><!-- Rating --></td></tr>
    </tbody>
  </table>
  <p><!-- Error message --></p>
  <p>No records found.</p>
</body>
</html>

ビューモデルを作成する

観測可能なオブジェクトは、Knockout のデータ バインディング システムの中核を担っています。観測可能なオブジェクトは、値を格納して、その値の変更時にサブスクライバーに通知できるオブジェクトです。次のコードでは、観測可能なオブジェクトを使用して、映画の JSON 表現を対応するオブジェクトに変換しています。

    function movie(data) {
      var self = this;
      data = data || {};
      // Data from model
      self.ID = data.ID;
      self.Title = ko.observable(data.Title);
      self.Year = ko.observable(data.Year);
      self.Rating = ko.observable(data.Rating);
      self.Genre = ko.observable(data.Genre);
    };

図 10 に、ビューモデルの初期実装を示します。このバージョンでは、映画の一覧を取得する機能だけをサポートしています。編集機能は後で追加します。ビューモデルには、映画の一覧、エラー文字列、および現在のジャンルに対応する観測可能なオブジェクトを用意しています。

図 10 ビューモデル

    var ViewModel = function () {           
      var self = this;
      // View model observables
      self.movies = ko.observableArray();
      self.error = ko.observable();
      self.genre = ko.observable();  // Genre the user is currently browsing
      // Available genres
      self.genres = ['Action', 'Drama', 'Fantasy', 'Horror', 'Romantic Comedy'];
      // Adds a JSON array of movies to the view model
      function addMovies(data) {
        var mapped = ko.utils.arrayMap(data, function (item) {
          return new movie(item);
        });
        self.movies(mapped);
      }
      // Callback for error responses from the server
      function onError(error) {
        self.error('Error: ' + error.status + ' ' + error.statusText);
      }
      // Fetches a list of movies by genre and updates the view model
      self.getByGenre = function (genre) {
        self.error(''); // Clear the error
        self.genre(genre);
        app.service.byGenre(genre).then(addMovies, onError);
      };
      // Initialize the app by getting the first genre
      self.getByGenre(self.genres[0]);
    }
    // Create the view model instance and pass it to Knockout
    ko.applyBindings(new ViewModel());

映画に observableArray を使用していることに注目してください。名前からわかるように、observableArray は、配列の内容の変更時にサブスクライバーに通知する配列として機能します。

getByGenre 関数は、サーバーに AJAX 要求を行って映画の一覧を取得し、結果を self.movies 配列に格納します。

REST API を使用するうえで最も難しい作業の 1 つは、HTTP の非同期的性質の処理です。jQuery ajax 関数は、Promises API を実装するオブジェクトを返します。Promise オブジェクトの then メソッドを使用して、AJAX 呼び出しが正常に完了した場合に呼び出すコールバックと、AJAX 呼び出しが失敗した場合に呼び出す別のコールバックを設定できます。

    app.service.byGenre(genre).then(addMovies, onError);

データ バインディング

ビューモデルが完成したので、HTML とビューモデルをデータ バインドできます。画面左側に表示するジャンル一覧には、次のデータ バインディングを使用しました。

<ul data-bind="foreach: genres">
  <li><a href="#"><span data-bind="text: $data"></span></a></li>
</ul>

data-bind 属性には、1 つ以上のバインディング宣言を指定し、それぞれのバインディングを "バインディング: 式" という形式にします。この例の foreach バインディングは、Knockout に対してビューモデルの genres 配列の要素をすべてループするよう指示しています。この結果、配列の要素ごとに Knockout によって新しい <li> 要素が作成されます。一方、<span> の text バインディングは、span のテキストが配列項目の値 (この場合はジャンル名) に等しくなるようを設定します。

この時点ではジャンル名をクリックしても何の処理も実行されないので、クリック イベントを処理する click バインディングを追加しました。

<li><a href="#" data-bind="click: $parent.getByGenre">
  <span data-bind="text: $data"></span></a></li>

このバインディングは、クリック イベントをビューモデルの getByGenre 関数にバインドします。ここで $parent を使用する必要があったのは、このバインディングが foreach のコンテキスト内で発生するためです。既定では、foreach 内のバインディングはループの現在の項目を参照します。

映画の一覧を表示するために、テーブルにバインディングを追加しました (図 11 参照)。

図 11 映画の一覧を表示するバインディングのテーブルへの追加

<table data-bind="visible: movies().length > 0">
  <thead>
    <tr><th>Title</th><th>Year</th><th>Rating</th><th></th></tr>
  </thead>
  <tbody data-bind="foreach: movies">
    <tr>
      <td><span data-bind="text: Title"></span></td>
      <td><span data-bind="text: Year"></span></td>
      <td><span data-bind="text: Rating"></span></td>
      <td><!-- Edit button will go here --></td>
    </tr>
  </tbody>
</table>

図 11 の foreach バインディングは、配列のすべての映画オブジェクトをループ処理します。foreach 内の text バインディングは、現在のオブジェクトのプロパティを参照します。

<table> 要素の visible バインディングは、テーブルをレンダリングするかどうかを制御します。movies 配列が空の場合、テーブルを非表示にします。

最後に、エラー メッセージと "No records found" メッセージのバインディングを以下に示します (バインディングには複雑な式も設定できます)。

    <p data-bind="visible: error, text: error"></p>
    <p data-bind="visible: !error() && movies().length == 0">No records found.</p>

レコードを編集可能にする

このアプリケーションの最後の部分は、ユーザーがテーブル内のレコードを編集できる機能です。この部分には、次のように複数の機能が必要です。

  • 表示モード (プレーン テキスト) と編集モード (入力コントロール) を切り替える。
  • 更新をサーバーに送信する。
  • ユーザーが編集を取り消して元のデータに戻せるようにする。

表示/編集モードを追跡するために、このアプリケーションでは観測可能なオブジェクトとして、ブール型フラグを movie オブジェクトに追加しました。

function movie(data) {
  // Other properties not shown
  self.editing = ko.observable(false);
};

今回は editing プロパティが false の場合は映画のテーブルを表示し、editing プロパティが true の場合は入力コントロールに切り替えることにしました。そのために、Knockout の if バインディングと ifnot バインディングを使用しました (図 12 参照)。<!-- ko --> 構文を使用すると、if バインディングと ifnot バインディングを、HTML コンテナー要素の中に配置しなくても有効にすることができます。

図 12 映画レコードの編集の有効化

<tr>
  <!-- ko if: editing -->
  <td><input data-bind="value: Title" /></td>
  <td><input type="number" class="input-small" data-bind="value: Year" /></td>
  <td><select class="input-small"
    data-bind="options: $parent.ratings, value: Rating"></select></td>
  <td>
    <button class="btn" data-bind="click: $parent.save">Save</button>
    <button class="btn" data-bind="click: $parent.cancel">Cancel</button>
  </td>
  <!-- /ko -->
  <!-- ko ifnot: editing -->
  <td><span data-bind="text: Title"></span></td>
  <td><span data-bind="text: Year"></span></td>
  <td><span data-bind="text: Rating"></span></td>
  <td><button class="btn" data-bind="click: $parent.edit">Edit</button></td>
  <!-- /ko -->
</tr>

value バインディングは、入力コントロールの値を設定します。これは双方向バインディングなので、ユーザーがテキスト フィールドに入力したりドロップダウンの選択項目を変更したりすると、変更が自動的にビューモデルに反映されます。

今回は、ボタンのクリック ハンドラーをビューモデルの save 関数、cancel 関数、および edit 関数にバインドしました。

edit 関数は単純で、editing フラグを true に設定するだけでした。

    self.edit = function (item) {
      item.editing(true);
    };

save 関数と cancel 関数はもう少し複雑でした。キャンセルをサポートするには、編集中に元の値をキャッシュする方法が必要でした。さいわい、Knockout の観測可能なオブジェクトの動作を拡張するのは簡単です。図 13 のコードでは、観測可能なクラスに store 関数を追加しています。観測可能なオブジェクトの store 関数を呼び出すと、観測可能な 2 つの新しい関数 (revert と commit) が追加されます。

図 13 revert と commit による ko.observable の拡張

これで、store 関数を呼び出して、この機能をモデルに追加できるようになりました。

function movie(data) {
  // ...
  // New code:
  self.Title = ko.observable(data.Title).store();
  self.Year = ko.observable(data.Year).store();
  self.Rating = ko.observable(data.Rating).store();
  self.Genre = ko.observable(data.Genre).store();
};

図 14 に、ビューモデルの save 関数と cancel 関数を示します。

図 14 保存機能とキャンセル機能の追加

    self.cancel = function (item) {
      revertChanges(item);
      item.editing(false);
    };
    self.save = function (item) {
      app.service.update(item).then(
        function () {
          commitChanges(item);
        },
        function (error) {
          onError(error);
          revertChanges(item);
        }).always(function () {
          item.editing(false);
      });
    }
    function commitChanges(item) {
      for (var prop in item) {
        if (item.hasOwnProperty(prop) && item[prop].commit) {
          item[prop].commit();
        }
      }
    }
    function revertChanges(item) {
      for (var prop in item) {
        if (item.hasOwnProperty(prop) && item[prop].revert) {
          item[prop].revert();
        }
      }
    }

Ember を使用して Web クライアントを構築する

比較のために、Ember.js ライブラリを使用した別のバージョンのアプリケーションを作成しました。

Ember アプリケーションでは、まずルーティング テーブルで、ユーザーがアプリケーション内を移動する方法を定義します。

    window.App = Ember.Application.create();
    App.Router.map(function () {
      this.route('about');
      this.resource('genres', function () {
        this.route('movies', { path: '/:genre_name' });
      });
    });

コードの 1 行目では、Ember アプリケーションを作成しています。また、Router.map を呼び出して、3 つのルートを作成します。各ルートは URI や URI パターンに対応しています。

/#/about
/#/genres
/#/genres/genre_name

ルートごとに、Handlebars テンプレート ライブラリを使用して HTML テンプレートを作成します。

Ember には、アプリケーション全体に適用できる最上位レベルのテンプレートがあります。このテンプレートは、すべてのルートでレンダリングされます。図 15 に、今回のアプリケーションのアプリケーション テンプレートを示します。ご覧のとおり、このテンプレートは基本的に HTML であり、type = "text/x-handlebars" という属性の script タグ内に配置します。テンプレート内には、{{}} という 2 重中かっこの中に特殊な Handlebars マークアップが含まれています。このマークアップは、Knockout の data-bind 属性と同じような役割を果たします。たとえば、{{#linkTo}} を使用すると、ルートへのリンクが作成されます。

図 15 アプリケーション レベルの Handlebars テンプレート

    ko.observable.fn.store = function () {
      var self = this;
      var oldValue = self();
      var observable = ko.computed({
        read: function () {
          return self();
        },
        write: function (value) {
          oldValue = self();
          self(value);
        }
      });
      this.revert = function () {
        self(oldValue);
      }
      this.commit = function () {
        oldValue = self();
      }
      return this;
    }
    <script type="text/x-handlebars" data-template-name="application">
      <div class="container">
        <div class="page-header">
          <h1>Movies</h1>
        </div>
        <div class="well">
          <div class="navbar navbar-static-top">
            <div class="navbar-inner">
              <ul class="nav nav-tabs">
                <li>{{#linkTo 'genres'}}Genres{{/linkTo}} </li>
                <li>{{#linkTo 'about'}}About{{/linkTo}} </li>
              </ul>
            </div>
          </div>
        </div>
        <div class="container">
          <div class="row">{{outlet}}</div>
        </div>
      </div>
      <div class="container"><p>&copy;2013 Mike Wasson</p></div>
    </script>

では、ユーザーが /#/about に移動したとしましょう。この結果 "about" ルートを呼び出すことになります。Ember では、まず最上位レベルのアプリケーション テンプレートがレンダリングされ、続いてアプリケーション テンプレートの {{outlet}} 内に about テンプレートがレンダリングされます。about テンプレートの内容は次のとおりです。

<script type="text/x-handlebars" data-template-name="about">
  <h2>Movies App</h2>
  <h3>About this app...</h3>
</script>

図 16 に、アプリケーション テンプレート内にレンダリングされた about テンプレートを示します。


図 16 about テンプレートのレンダリング

各ルートに独自の URI があるので、ブラウザー履歴が保存されます。ユーザーは、戻るボタンを使用して移動できます。また、ユーザーがページを更新してもコンテキストは失われず、お気に入りに追加して同じページを再読み込みすることもできます。

Ember のコントローラーとモデル

Ember では、ルートごとに専用のモデルとコントローラーが存在します。モデルにはドメイン データが含まれています。コントローラーはモデルのプロキシとして機能し、ビューに関するあらゆるアプリケーション状態データを格納します (これは MVC の従来の定義と少し異なっており、ビューモデル寄りのコントローラーとも言えます)。

映画モデルの定義方法を以下に示します。

    App.Movie = DS.Model.extend({
      Title: DS.attr(),
      Genre: DS.attr(),
      Year: DS.attr(),
      Rating: DS.attr(),
    });

今回のコントローラーは、Ember.ObjectController から派生しています (図 17 参照)。

図 17 Ember.ObjectController から派生した映画コントローラー

    App.MovieController = Ember.ObjectController.extend({
      isEditing: false,
      actions: {
        edit: function () {
          this.set('isEditing', true);
        },
        save: function () {
          this.content.save();
          this.set('isEditing', false);
        },
        cancel: function () {
          this.set('isEditing', false);
          this.content.rollback();
        }
      }
    });

ここではいくつか興味深い処理を実行しています。まず、モデルをコントローラー クラスに指定しませんでしたが、既定でルートによって自動的にモデルがコントローラーに設定されます。次に、save 関数と cancel 関数で、DS.Model クラスに組み込まれているトランザクション機能を使用しています。編集を元に戻すには、モデルの rollback 関数を呼び出すだけです。

Ember では、さまざまなコンポーネントを結び付けるために、多数の名前付け規則を使用します。genres ルートとやり取りする GenresController によって、genres テンプレートがレンダリングされます。実際、GenresController オブジェクトは、開発者が定義しなくても Ember によって自動的に作成されます。ただし、既定の GenresController をオーバーライドすることもできます。

今回のアプリケーションでは、renderTemplate フックを実装して、別のコントローラーを使用するよう genres/movies ルートを構成しました。このようにすると、複数のルートで同じコントローラーを共有できます (図 18 参照)。

図 18 複数のルートで同じコントローラーを共有できる

App.GenresMoviesRoute = Ember.Route.extend({
  serialize: function (model) {
    return { genre_name: model.get('name') };
  },
  renderTemplate: function () {
    this.render({ controller: 'movies' });
  },
  afterModel: function (genre) {
    var controller = this.controllerFor('movies');
    var store = controller.store;
    return store.findQuery('movie', { genre: genre.get('name') })
    .then(function (data) {
      controller.set('model', data);
  });
  }
});

Ember の長所の 1 つは、処理に必要なコードが少ないことです。今回の Ember によるサンプル アプリケーションは、約 110 行の JavaScript で構成されています。これは Knockout バージョンのサンプルよりも短いうえ、ブラウザー履歴も自由に取得できます。一方で、Ember は非常にこだわりの強いフレームワークでもあります。"Ember の流儀" でコードを作成しない場合、障害が発生するおそれが高まります。フレームワークを選択する際は、フレームワークの機能セットと全体的な設計が、自分のニーズやコーディング スタイルに合うかどうか検討してください。

詳細情報

この記事では、複数の JavaScript フレームワークを使用して SPA の作成を簡略化する方法を示しました。その過程で、データ バインディング、ルーティング、MVC パターン、MVVM パターンなど、これらのライブラリに共通する機能をいくつか紹介しました。ASP.NET を使用した SPA の構築の詳細については、asp.net/single-page-application (英語) を参照してください。

Mike Wasson は、マイクロソフトのプログラマ兼ライターです。長年の間、Win32 マルチメディア API のドキュメントを作成してきました。現在は、Web API に焦点を絞った ASP.NET に関する記事を執筆しています。連絡先は mwasson@microsoft.com (英語のみ) です。

この記事のレビューに協力してくれた技術スタッフの Xinyang Qiu (マイクロソフト) に心より感謝いたします。
Xinyang Qiu は、Microsoft ASP.NET チームのテストを担当するシニア ソフトウェア設計エンジニアであり、blogs.msdn.com/b/webdev (英語) で積極的にブログを執筆しています。趣味は、ASP.NET についての質問に回答したり、寄せられた質問に回答する専門家を紹介することです。連絡先は xinqiu@microsoft.com (英語のみ) です。