『AngularJS: Up and Running』読書メモ 第10章 ngRouteによるルーティング

AngularJS: Up and Running

AngularJSはngRouteというモジュールをオプションで提供している。これを使うと、AngularJSアプリケーションでルーティング(URLに応じた画面遷移)を実装することができる。

※本書が執筆された当時はngRouteが唯一の公式ルーターだったが、2015年にNew Routerというルーターが導入されている。このルーターはAngularJS の1.4以上と2系のいずれでも使えるという特徴がある。

シングルページアプリケーションのルーティング

シングルページアプリケーションにおけるルーティングにおいては、URLとは通常のURLではなく、シバンの追加されたURLである。通常のURLは以下のようなスタイルである。

  • http://example.com/first/page

これに対して、シングルページアプリケーションのURLは以下のようになる。

  • http://example.com/#/first/page
  • http://example.com/#!/first/page

このようになる理由は、ブラウザはハッシュを含むURLを、含まないURLとは違った形で解釈するからである。ブラウザは、#の後ろにある部分をサーバへのリクエストの際に無視する。そのため、#の後ろの部分が変化しても、サーバへのリクエストは行われない。

ngRouteを使う

AngularJSのルーティングモジュールを使う手順は以下のとおり。

1 モジュールのソースコードを読み込む

<script type="text/javascript" src="/PATH/TO/angular-route.min.js">

2 モジュールの依存関係を定義する

angular.module("myApp", ["ngRoute"]);

3 ページ内のどの部分をAngularJSが変更すべきか印をつける。ngRouteを使う場合、ng-viewディレクティブをHTML内で使う。

4 $routeProviderサービスを使ってconfigセクションでルーティングを定義する

<body ng-app="routingApp">
  <a href="#/">Default route</a>
  <a href="#/second">Second route</a>
</body>
angular.module('routingApp', ['ngRoute'])
  .config(['$routeProvider', function($routeProvider) {
    $routeProvider.when('/', {
      template: '<h5>This is the default route</h5>'
    })
    .when('/second', {
      template: '<h5>This is the second route</h5>'
    })
    .otherwise({redirectTo: '/');
  }]);

$routeProviderを使うことで、各ルートをwhen()メソッドで定義できる。when()メソッドは、2つの引数をとる。

  • 第1引数はURL、またはこのルートを適用すべきURLの正規表現
  • 第2引数は設定オブジェクトで、そのルートで何が起きるべきかを指定する

otherwize()関数を使うことで、未定義のルートにユーザが遷移しようとした時の振る舞いを決めることができる。

ルーティングオプション

$routeProvider.whenメソッドの第2引数には以下のような形でオプションを渡すことができる。

$routeProvider.when(rul, {
  template: string,
  templateUrl: string,
  controller: string/function/array,
  controllerAs: string,
  redirectTo: string,
  resolve: object<key, function>
});
  • url: URLまたはURLの正規表現。/user/:userId のようにプレースホルダーを設定することも可能
  • template: HTML文字列
  • templateUrl: テンプレートへのパス
  • controller: コントローラー名の文字列又はコントローラーを定義する関数・配列
  • controllerAs: テンプレート内でアクセスするためのコントローラーの別名
  • redirectTo: 別のルートにリダイレクトする
  • resolve: ルートが実行される前に実行される非同期の処理を記述する(認可、認証等に使う)

resolveによるルート実行前のチェック

resolveによって、ルートが読み込まれる前に実行される非同期の処理を定義することができる。

angular.module('resolveApp', ['ngRoute'])
  .value('Constant', {MAGIC_NUMBER: 42})
  .config(['$routeProvider', function($routeProvider) {
    $routeProvider.when('/', {
      template: '<h1>Main Page, no resolves</h1>'
    }).when('/protected', {
      template: '<h2>Protected Page</h2>',
      resolve: {
        immediate: ['Constant', function(Constant) {
          return Constant.MAGIC_NUMBER * 4;
        }],
        async: ['$http', function($http) {
          return $http.get('/api/hasAccess');
        }]
      },
      controller: ['$log', 'immediate', 'async',
        function($log, immediate, async) {
          $log.log('Immediate is ', immediate);
          $log.log('Server returned for async', async);
      }]
    });
}]);

2番目のルート/protectedには、resolveが定義されている。連想配列にimmediateasyncというキーが定義されているが、キーには任意の値を使用できる(myKey1, myKey2でも何でもいい)。連想配列の値は、AngularJSのDIのシンタックスの配列になる。

ルーティングのresolveについて、AngularJSは以下を保証する。

  • resolveが値を返した場合、AngularJSはこの関数の実行を完了し、成功したresolveとして扱う
  • resolveがpromiseオブジェクトを返した場合、AngularJSはpromiseが値を返すのを待って、promiseの結果に応じて成功/失敗として扱う
  • 全てのresolve関数が完了するまで、ルートは読み込まれない
  • 全てのresolveは並列に実行される
  • いずれかのresolveがエラーに遭遇するか、promiseがrejectされたら、AngularJSはそのルートを読み込まない

上のサンプルコードでは、immediateのresolveは、定数を返すだけなので、毎回成功する。asyncはサーバとの通信を行い、成功すれば、ルートが読み込まれる。失敗すると、コントローラーは読み込まれず、対応するHTMLも表示されない。この場合、ユーザーは最後に表示されたページに留まる。

もう一点重要なことは、それぞれのresolveの戻り値は、それぞれのキーによってコントローラーに注入される、ということだ。上のサンプルコードでは、immediateは数値、asyncはpromiseオブジェクトになる。

$routeParamsサービスを使う

Single Page Application(SPA)では、ルートのコンテキストが必要になることが多い。たとえば、/users/1に対して、id=1のユーザの情報を表示する、といった処理が必要になる。

理想的には、コントローラーとルートは、独立してアクセス可能であるべきである。これらのURLパラメータには、$routeParamsサービスを使ってアクセスできる。

$routeProvider.when('/detail/:detId', {
    template: '<h2>Loaded {{myCtrl.detailId}}' +
        ' and query String is {{myCtrl.qStr}}</h2>',
    controller: ['$routeParams', function($routeParams) {
        this.detailId = $routeParams.detId;
        this.qStr = $routeParams.q;
    }],
    controllerAs: 'myCtrl'
});

このルートにおける、:detIdはプレースホルダーである。ここにある値は$routeParams.detIdで取得できる。

※ngRouteにはURLに制約を付ける機能がないため、/detail/1だけでなく/detail/fooでもアクセスできる。後者の場合、detIdは’foo’になる。

また、URLに含まれるクエリストリングも取得できる。/detail/1?q=fooにアクセスすると、以下のようなデータが取得できる。

{
  detId: '1',
  q: 'foo'
}

注意すべきこと

空のテンプレート

ngRouteは、それぞれのルートが空ではないtemplateまたはtemplateUrlに関連付けられていることを要求する。テンプレートが存在しない場合、ngRouteはこのルートを無視する。空文字列のtemplateも、テンプレートが存在しないとみなされるので注意が必要である。

resolveによるコントローラーへの注入

resolveを使ってコントローラーに値を注入する際は、ng-controllerディレクティブを使用せず、ルーティング定義の一部としてコントローラーを定義すること。そうしないと、AngularJSは依存している値が取得できない。

$routeParamsの値の型

$routeParamsから取得した値は、デフォルトでは全て文字列になる。

1つのアプリケーションにng-viewは1つだけ

ngRouteでは、ng-viewは1つだけ使用可能である。複数のng-viewや、ネストしたng-viewは使用できない。

追加の設定

HTML5モード

ハッシュの含まれるURLはSPAでは一般的なものであるが、ハッシュが含まれない「普通の」URLを使うこともできる。

AngularJSのHTML5モードは、ブラウザのpushState APIを使用する。http://example.com/#/user/1というURLは、HTML5モードではhttp://example.com/user/1となる。

HTML5モードを有効化するには、サーバ側のサポートも必要である。ユーザがhttp://example.com/user/1にアクセスしたとき、サーバのドキュメントルート配下の/user/1ではなく、index.htmlが返るようにする必要がある。

HTML5モードを有効化するには、以下の3つが必要である。

(1) AngularJSのconfigでHTML5モードを有効化する

angular.module('app')
  .config(['$locationProvider', function($locationProvider]) {
    $locationProvider.html5Mode(true);
    $locationProvider.hashPrefix('!');
});

HTML5モードを有効化するには、$locationProviderを使用する。hashPrefixに’!’を設定することが、SEO上の観点からは推奨される。

(2) <base />タグ

index.html<head>の中に、href属性を含んだ<base />タグを追加する必要がある。これによって、ブラウザに、画像やCSS等の静的リソースをどこから取得すべきかを教える。

たとえば、アプリケーションがhttp://example.com/appというURLで提供されていてる場合、HTML5モードが有効になっていると、URLはhttp://example.com/app/user/1といった形になる。このとき、サーバはindex.htmlを返すが、ブラウザのパスは/app/user/1なので、相対パスではファイルを取得できない。以下のように設定されていれば、この問題を解決できる。

<html>
  <head>
    <base href="/app" />
  </head>
</html>

これによって、URLに関わらず、相対パスが/appを起点として解決されるようになる。

(3) サーバサイド

/first/page/second/pageといったルートに対するアクセスの場合にも、index.htmlが返されるようにする必要がある。

以下はNodeJSの場合の実装例。

var express = require('express'),
        url = require('url');

var app = express();

// express configuration here

var INDEX_HTML = fs.readFileSync(
  __dirname + '/index.html', 'utf-8');

var ACCEPTABLE_URLS = ['/first/page', '/second/page'];

app.use(function(req, res, next) {
  var parts = url.parse(req.url);
  for (var i = 0; i < ACCEPTABLE_URLS.length; i++) {
    if (parts.pathname.indexOf(ACCEPTABLE_URLS[i]) === 0) {
      // We found a match to one of our
      // client-side routes
      return res.send(200, INDEX_HTML);
    }
  }
  return next();
});

// Other routes here

AngularJSとSEO

検索エンジンについて気に留めておくべき点は以下のとおり。

  • GoogleやBingのような検索エンジンは、SPAに対応したパターンを有している。これらは、hashbangを使用したURLの使用を前提にしている(#ではなく、#!を使用したURL)
  • 検索エンジンがクロールする際、#!?escaped_fragment=に置き換えて、サーバにリクエストを送る

さらに、サーバーが検索エンジンからのリクエストを判別して、以下のような手段を使用してレスポンスを返す必要がある。

  • HTMLのスナップショットを作成して返す: AngularJSのドキュメントはこの方法を使用している
  • HTMLをリアルタイムにレンダリングして返す: PhantomJSのようなヘッドレスブラウザをサーバで使用してHTMLをレンダリングする

angular-seoのようなツールの使用も検討すべきである。

AngularJSと分析サービス

Google Analyticsのような分析サービスは、SPAでは簡単には動かない。分析サービスは、ページの読み込み時のイベントにフックして処理を行っているからである。SPAでは、Google Analyticsを手動で呼び出してルートの変更を教える必要がある。これには、Angularyticsのようなライブラリが使用できる。

代替となるルーター

ngRouteで、70-80%のニーズは満たすことができる。SPAのURLは一般的にシンプルなものになるからである。

しかし、URLに複雑な要求があったり、URLに応じてUIを細かに制御したいような場合には、ngRouteの機能では対応が難しい。

このような場合には、ui-routerの使用を検討すべきである。

※2016年3月現在では、New Routerも代替候補の1つになる。

コメントをどうぞ

コメントを残す