ほげにっき

hogedigoの日記

AngularJSチュートリアルやってメモメモ(step7 - ルーティング&マルチビュー)

Angularの公式ドキュメントやチュートリアルの日本語翻訳作業が進んでいることを教えて頂いた。
早くも無駄な作業になってしまった気がするが、途中までやって止めるのもなんだし(いわゆるサンクコストか)自分の学習の為に最後までやることにする^^;

目次


今回やるのはこちら↓
AngularJS: 7 - Routing & Multiple Views


このstepではレイアウトテンプレートの作成方法と、ルーティング機能を利用してマルチビューを持つアプリを作る方法を学ぶ。


ワークスペースをstep7のにリセット。注:ローカルの修正は破棄される

git checkout -f step-7

ローカルサーバーをリフレッシュ(またはAngularサーバーで確認可)。
ブラウザでapp/index.htmlにアクセスするとapp/index.html#phonesにリダイレクトされ、前回と同様の端末リストが表示される。端末リンクをクリックすると端末詳細ページのスタブが表示される。


step6からstep7への変更を説明。diffはこちら

マルチビュー、ルーティング&レイアウトテンプレート

step7以前は単一のビュー(端末リスト)のみで構成され、全てのテンプレートコードはindex.htmlに記述されていた。このステップではリスト各端末の詳細情報を表示するビューを追加する。

index.htmlファイルを拡張して両方のビューのテンプレートコードを含めることも可能だが、それだとコードがすぐに複雑になってしまう。代わりに、index.htmlを、いわゆる「レイアウトテンプレート」として扱う。これはアプリケーションの全てのビューの為の共通となるテンプレートとなる。他の「部分テンプレート」は現在の「ルート(ユーザーに現在表示されているビュー)」に基づいてレイアウトテンプレートにインクルードされる。

Angularのアプリケーションルートは$routeサービスのプロバイダーである$routeProviderを通して宣言される。このサービスを使用することでコントローラー、ビューテンプレートと現在のブラウザのURLを結びつけて記述することが可能となる(かなり意訳。原文よく分からんかった-_-;)。この機能を用いてディープリンキング(ブラウザのヒストリーフォワード&バックや、ブックマークの制御)を実装することが出来る。

DIに関してメモ。インジェクターとプロバイダー

依存性注入(DI)はAngularJSのコア機能なので、この辺でその挙動を少し理解しておいた方がよい。

アプリケーションが初期起動される時、Angularはアプリの全てのDI関連機能で使用されるインジェクターを作成する。インジェクターそれ自身は$httpや$routeサービスなどが何をするかは知っていないし、それどころかそれらのサービスの存在すら(固有モジュール定義で設定されない限りは)知らない。インジェクターの役割は、指定されたモジュールの定義をロードして、それらのモジュールに定義されている全てのサービスプロバイダーを登録し、そして要求されれば指定されたfunctionの引数にプロバイダによって遅延初期化された依存コンポーネント(サービス)を注入することだ。

プロバイダーはサービスのインスタンスを作成・提供する役割を持ち、またそれらサービスの生成と挙動を制御する為の設定APIを公開する。例として$routeサービスの場合、$routeProviderプロバイダーはアプリケーションのルートを定義する為のAPIを公開している。

Angularのモジュールはアプリケーションのグローバル状態を取り除くという課題を解決し(scopeのことかな?)、インジェクターを設定する手段を提供する。AMDやrequire.jsと違ってAngularモジュールはスクリプトのロード順や遅延フェッチの問題を解決しようとはしていない。これらのモジュールシステムらの目的とAngularモジュールの目的は直交しており、それぞれを満たす為に併用すればよい。

アプリケーションモジュール

app/js/app.js:

angular.module('phonecat', []).
  config(['$routeProvider', function($routeProvider) {
  $routeProvider.
      when('/phones', {templateUrl: 'partials/phone-list.html',   controller: PhoneListCtrl}).
      when('/phones/:phoneId', {templateUrl: 'partials/phone-detail.html', controller: PhoneDetailCtrl}).
      otherwise({redirectTo: '/phones'});
}]);

アプリケーションにルートを設定する為には、まずアプリケーションモジュールを作成する必要がある。'phonecat'という名前でモジュールを作成し、config APIを使用して$routeProviderをこちらの設定用関数にDIし、その関数の中で$routeProvider.when APIを呼び出してルートを定義している。

インジェクター設定フェーズの間はプロバイダーは上記の様にDIさせることが出来るが、インジェクターが既に作成されサービスインスタンスの作成を開始した後はDIさせることが出来なくなるので注意。

サンプルアプリケーションのルートは下記の様に定義されている。

  • URLのハッシュが'/phones'の場合は端末リストviewを表示。viewを構築するのにAngularはphone-list.htmlテンプレートとPhoneListCtrlを使用する。
  • ハッシュが'/phone/:phoneId'の場合は'端末詳細viewを表示する。「:phoneId」という部分は動的な変数パートとなりURLの該当部分がルート変数(後述)phoneIdに格納される。端末リストviewを構築するのにAngularはphone-detail.htmlテンプレートとPhoneDetailCtrlコントローラーを使用する。

前回のステップで作成したPhoneListCtrlコントローラーを使い、端末詳細ビューの為の空の新しいPhoneDetailCtrlコントローラーをapp/js/controller.jsファイルに追加する。

$route.otherwise({redirectTo: '/phones'}) という文はブラウザのアクセスされたアドレスがどのルートにもマッチしなかった場合に/phonesへのリダイレクトさせることを意味する。

2番目のルート宣言の「:phoneId」パラメータに注目。$routeサービスは'/phones/:phoneId'をURLとマッチさせる為のテンプレート文字列として使用する。「:notation」という書式で定義された全ての変数は$routeParamsオブジェクトに展開される。

新しく作成したモジュールでアプリケーションを起動する為に、ngAppディレクティブでモジュール名を指定する必要がある。

app/index.html:

<!doctype html>
<html lang="en" ng-app="phonecat">
...

コントローラー

app/js/controllers.js:

...
function PhoneDetailCtrl($scope, $routeParams) { 
  $scope.phoneId = $routeParams.phoneId;
}
 
//PhoneDetailCtrl.$inject = ['$scope', '$routeParams'];

テンプレート

$routeサービスは通常ngViewディレクティブと併せて使用される。ngViewディレクティブの役割はレイアウトテンプレート内に現在のルートのビューテンプレートをインクルードすることだ。

app/index.html:

<html lang="en" ng-app="phonecat">
<head>
...
  <script src="lib/angular/angular.js"></script>
  <script src="js/app.js"></script>
  <script src="js/controllers.js"></script>
</head>
<body>
 
  <div ng-view></div>
 
</body>
</html>

index.htmlから殆どのコードが取り除かれ、ng-view属性を持つdivタグだけのシンプルな1行に置き換わっていることに注目。取り除かれたコードはphone-list.htmlテンプレートに移されている。

app/partials/phone-list.html:

<div class="container-fluid">
  <div class="row-fluid">
    <div class="span2">
      <!--Sidebar content-->
 
      Search: <input ng-model="query">
      Sort by:
      <select ng-model="orderProp">
        <option value="name">Alphabetical</option>
        <option value="age">Newest</option>
      </select>
 
    </div>
    <div class="span10">
      <!--Body content-->
 
      <ul class="phones">
        <li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
          <a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
          <a href="#/phones/{{phone.id}}">{{phone.name}}</a>
          <p>{{phone.snippet}}</p>
        </li>
      </ul>
 
    </div>
  </div>
</div>

phone-detail.htmlテンプレートには今のところシンプルに端末IDのみを表示している。
app/partials/phone-detail.html:

TBD: detail view for {{phoneId}}

PhoneDetailCtrlコントローラーで定義されているphoneIdモデルを使用している。

テスト

全てが適切に結合されていることを自動検証する為に、いくつかのURLにナビゲートして正しいビューが表示されるかどうかをチェックするend-to-endテストを書く。

...
  it('should redirect index.html to index.html#/phones', function() {
    browser().navigateTo('../../app/index.html');
    expect(browser().location().url()).toBe('/phones');
  });
...
 
 describe('Phone detail view', function() {
 
    beforeEach(function() {
      browser().navigateTo('../../app/index.html#/phones/nexus-s');
    });
 
 
    it('should display placeholder page with phoneId', function() {
      expect(binding('phoneId')).toBe('nexus-s');
    });
 });

./scripts/e2e-test.shまたは、ent-to-endテストランナーのブラウザタブを更新してテストを実行する。またはAngularのサーバーでテスト実行を確認できる。

実験

index.htmlに{{orderProp}}バインディングを追加して、端末リストビューで何も起きないことを確認しよう。これはorderPropモデルが要素に結びついているPhoneListCtrlコントローラーのスコープでのみ参照可能だから。phone-list.htmlテンプレートに同じバインディングを追加してみると期待通りに動作する。


今日はここまで。次回は「step8 - さらにテンプレート」で端末詳細ビューを実装する。