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
が定義されている。連想配列にimmediate
とasync
というキーが定義されているが、キーには任意の値を使用できる(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つになる。