2015/03/21に開催された ng-japan にて、AngularJS New Routerの話を聞いてきたので、触りがてらのメモ.
new Routerとは
セッションで講演されていた Brian Fold 曰く:
- Angular 1.4(もうすぐリリース予定)と併せてリリースされる, Angularの新しいRouting機構
- Angular 2.x / 1.x の両方で動作することを目標としている.
- ngRouterだけでなく、ui-routerのように広く利用されているルーティングモジュール開発者の意見を取り入れながら開発した.
何はともあれ触ってみる
手っ取り早く触ってみるのであれば、npm でnew Routerをinstallしてみるのがよい.
npm install angular-new-router
cd node_modules/angular-new-router
npm install
node_modules/angular-new-router
をcwdにして、WebServerを立ち上げる.
npm install -g http-server
http-server
これでブラウザから, http://localhost:8080/examples/angular-1/ を叩けば、exampleの一覧が確認できる.
ルーティングの基本
ここでは、example/angular-1/animation
を例にソースコードを読んでいく.
angular.module('example', [
'example.goodbye',
'example.welcome',
'ngAnimate',
'ngNewRouter'
])
.controller('AppController', ['$router', AppController]);
AppController.$routeConfig = [
{ path: '/', redirectTo: '/welcome' },
{ path: '/welcome', component: 'welcome' },
{ path: '/goodbye', component: 'goodbye' }
];
function AppController($router) {
this.greeting = 'Hello';
}
上記が、アプリの起点となるコンポーネントだ.
new Routerを利用する場合、モジュール定義時に 'ngNewRouter'
モジュールへの依存関係を読み込ませてやる必要がある.
何故そこに$routeConfigを?
特徴的なのが、AppController.$routeConfig=...
の部分だ。
Controllerのプロパティとして、ルーティングの定義を記述している。
AngularJSの主要なルーティングモジュールの場合, 下記のように .config
と Providerを組み合わせてルーティング定義するのがスタンダートであったことを考えると、new Routerの定義方法は一見すると奇妙だ。
angular.modlue('app', ['ui.router']).config(function ($stateProvider) {
$stateProvider.state('welcome', {url: '/welcome', ...});
});
これは、Angular 2.x を見越してのことと考えると納得できる。実際、セッションの発表でもあったのだが、先ほどのAppControler相当は、Angular 2.xでは下記のように書くことが出来る。
@Component({selector: 'my-app'})
@Template({url: 'myApp.html'})
@RouteConfig([
{ path: '/', redirectTo: '/welcome' },
{ path: '/welcome', component: 'welcome' },
{ path: '/goodbye', component: 'goodbye' }
])
class AppComponent {
greeting: string;
constructor() {
this.greeting = 'Hello';
}
}
Angular 2.xでは、Componentに対して、アノテーション(またはRuntime Decorator)で、様々な情報をフレームワーク側に伝えることが出来るようになる。classに付与したアノテーションはTypeScript(最早AtScriptと呼ぶ必要はなくなりましたね)によって、classのstaticなプロパティとして展開される.
このため、1.x / 2.x の双方で動作するルーティング機構となると、ルーティング定義をControllerのプロパティとして与えるようなコーディングになる訳だ。
逆に、1.xでのProviderを使った.config
自体, Angular 2.xでは存在しなくなる(@shuhei さん, ご指摘有難うございます).
Componentとネーミングルール
続いて、親から参照される各Component側のコードだ.
angular.module('example.welcome', []).
controller('WelcomeController', WelcomeController);
function WelcomeController() {
this.heading = 'Welcome to The New Angular Router Demo!';
}
<section>
<h2>{{welcome.heading}}</h2>
</section>
(goodbye.js, goodbye.htmlについてもほぼ同様なので割愛)
new Routerは"Component"と呼ばれる単位でルーティングを制御している。
"Component" とは、
- Angular 1.xでは、Controller + Template HTMLのセットと考えればよい。
- Angular 2.xでは
@Component(...)
が付与されたclassであり、テンプレートとの紐付けも@Template
で制御する模様.
(少なくとも)Angular 1.xでnew RouterにComponentを登録するために重要なのがネーミングルールだ.
最低限、下記のルールを守る必要がある.
Component名を'xxxYyy'
としたとき、
- Componentに対応するControllerは、
.controller
での登録時、XxxYyyController
という名前にしなくてなはならない。
(xxxCtrl
とかxxx
だと、new Routerがwarnを上げてくる) - ComponentのViewとなるHTMLについては、
components/xxx-yyy/xxx-yyy.html
に配置する必要がある - Template HTMLからは、Component名(=
xxxYyy
)が自動的にcontrollerAs名として利用できるようになっている.
例えば、Component名が'myWidget'
の場合は下記となる:
- Controller:
controller.('MyWidgetController', ...)
で登録. - Template HTML:
./components/my-widget/my-widget.html
に配置. - Templateで利用可能なcontrollerAs名:
myWidget
このようなネーミングルールが何故存在するかについては、考えてみると当たり前のことで、.$routeConfig
で教えたルーティング定義は、他の機構に比べると情報が少なすぎるのだ。
AppController.$routeConfig = [
{ path: '/', redirectTo: '/welcome' },
{ path: '/welcome', component: 'welcome' },
{ path: '/goodbye', component: 'goodbye' }
];
ngRouterにしろ、ui-routerにしろ、ルーティング定義時には、下記の3点が必要だったことを考えると、定義情報が少ない。
- URLのパスフラグメント
- ng-view(またはui-view)に展開されるhtmlのURL
- 管理するController名
旧来のルーティングモジュールから減った情報を補うため、new Routerではネーミングルールが採用されており, Component名が確定すれば、自動的にTemplate HTMLのURLとController名が確定する仕組みとなっている.
なお、自分では試していないが、$componentLoaderProvider
でこれらのネーミングルールは変更可能なようだ。
angular.module('app', ['ngNewRouter']).config(function ($componentLoaderProvider) {
$componentLoaderProvider.setCtrlNameMapping(function (componentName) {
// component名が'xxx'の場合に、Controller xxxCtrlと紐付ける
return componentName + 'Ctrl';
});
});
viewport, deep-linking
さて、今までに説明してきた.js等を読み込み、Componentの表示領域を<ng-viewport>
というDirectiveで用意してやったのが、下記のindex.htmlである:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<base href="/examples/angular-1/animation/">
<link rel="stylesheet" href="./app.css">
<title>Routing</title>
</head>
<body ng-app="example" ng-strict-di ng-controller="AppController as app">
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><a ng-link="welcome">welcome</a></li>
<li><a ng-link="goodbye">goodbye</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="loader" ng-if="router.isNavigating">
<i class="fa fa-spinner fa-spin fa-2x"></i>
</li>
</ul>
</div>
</nav>
<div class="page-host">
<ng-viewport></ng-viewport>
</div>
<script src="/node_modules/angular/angular.js"></script>
<script src="/node_modules/angular-animate/angular-animate.js"></script>
<script src="/dist/router.es5.js"></script>
<script src="./app.js"></script>
<script src="./components/goodbye/goodbye.js"></script>
<script src="./components/welcome/welcome.js"></script>
</body>
</html>
<a>
タグに付与されている ng-link
Directiveには、行き先のComponent名を指定してやることで、Componentに対応するURLフラグメントのhrefに展開される(deep-linking).
この辺りの機構は、ui-routerを触ったことがある人であれば、ui-view
やui-sref
の動きと一緒なので、違和感はないと思う。
URL Parameter
ngRouterやui-routerと同様、URLにパラメータが利用できる.
/:変数名
とすることで、Path Parameterを定義:
angular.module('app', ['ngNewRouter'])
.controller('AppController', ['$router', AppController]);
function AppController ($router) {
$router.config([
{ path: '/', component: 'home' },
{ path: '/detail/:id', component: 'detail' }
]);
}
ui-sref
と同じく、遷移時に引数としてパラメータを渡せる:
<a ng-link="detail({id: 5})">link to detal</a>
Componentからは、$routeParams
でパラメータ値を取得出来る:
angular.module('app.detail', ['ngNewRouter'])
.controller('DetailController', ['$routeParams', DetailController]);
function DetailController ($routeParams) {
this.id = $routeParams.id;
}
Multi-View
先ほどの例では、ng-viewport
が1つであったが、複数のviewportをpathに割り当てることもできる.
$router.config([{
path: '/home',
components: {
main: 'list',
sub: 'result'
}
}]);
<div ng-viewport="main" ></div>
<div ng-viewport="sub" ></div>
こちらもURLパラメータと同様、ui-routerに慣れていると特に違和感なし.
Componentの入れ子
ui-routerで実現できていた、State定義時の 'view名@所属state名'
に相当する機能があるか、等が気になる.
何か分かったら後で追記します.
Fix後削除予定:
3/23 現在, ComponentとなっているテンプレートのHTML中にng-viewport
をネストすると, ngNewRouter内部でエラーが発生する。issue:193 で取り上げられており, v0.5.1でfixされる予定.
Componentをネストする際の挙動については、 issue:117, issue:127 で議論している最中.
LifeCycle Hook
new Routerはルーティング前後に処理を挟み込んだりするための拡張ポイントが用意されている.
- canActivate: このComponentへ遷移する直前. 「このComponentに遷移する権限があるか」のように、認可処理に用いると便利.
- activate: このComponentへ遷移した直後. Ajaxによるデータ取得など、ある程度「重たい」処理を初期処理として挟み込むとよい.
- canDeactivate: このComponentから遷移する直前. 終了確認等に用いるとよい.
- deactivate: このComponentから遷移した直後.
これらの拡張ポイントへの処理追加は、Componentに上記名称のメソッドを定義することで実現する。すなわち、下記のようなコードとなる(メソッド定義に.prototype
とか出てくる辺りが、Angular 2.xのClass記法を強く意識してるのが分かりますね).
function MyController(user, $http) {
this.user = user;
this.$http = $http;
}
MyController.prototype.canActivate = function() {
return this.user.isAdmin;
};
MyController.prototype.activate = function() {
return this.bigFiles = this.$http.downloadBigFiles();
};
oldComponent→newComponentへ遷移する場合、以下の順序でこれらの拡張ポイントが作動する。
- (遷移開始)
- oldComponent.canDeactivate
- (newComponentのインスタンス作成)
- newComponent.canActivate
- oldComponent.deactivate
- newComponent.activate
- (遷移完了)
これらの拡張ポイントは、下記のいずれかを返却すると、遷移をキャンセルすることが出来る.
false
- promiseオブジェクト(その後rejectされた場合)
- promiseオブジェクト(その後
resolve(false)
された場合)
なお、ComponentのコンストラクタでErrorをぶん投げるような荒技を使って遷移をキャンセルすることも出来るが、「Constructorに遷移制御の処理を突っ込むな」とのこと(Guideより)。
僕はui-router大好きっ子なので、遷移時のフックは主に下記で実現していた。
- 遷移自体のキャンセル:
$stateChangeStart
イベントをリスンし、必要であれば、$state.go()
実行 - 遷移直後に必要となるデータの取得:
$resolve
で遷移先のControllerにインジェクション.
new Routerでは、1. のパターンは.canDeactivate
or .canActivate
を使い、2. のケースでは.activate
を使うのがよいのだろう。
(厳密には、Componentのインスタンス化より手前の処理実行は存在しないため、resolve相当のタイミングは存在しないと思われるが、拡張ポイントがインスタンスメソッドになっていることを考えると、「インスタンス化より前に動作するポイント」があるのも変だしなぁ。。。)
ui-routerには、StateにonEnter
やonExit
ハンドラも用意されており(僕はあまり使っていない)、それぞれactivate, deactivateメソッドが対応していると思えば、利用している人は乗り換え易いのではないだろうか。
なお、全く関係ないかもしれないが、ComponentとなるClassにライフサイクルに関連する拡張ポイントが用意される、というのを見て、ReactJSのcomponentWillMountやcomponentDidMountのようなメソッドを思い浮かべたのは僕だけだろうか...
すぐに使えるか?
ui-routerを捨てて、new Routerに乗り換えるか、と問われると、現時点では何ともというのが正直な感想.
Lifecycle Hookやアノテーションを見据えたComponent志向なのは別に構わないのだけど、ui-routerの"State"という概念が消滅してしまっているのは正直辛いかも.
また、現状で 親 - 子 - 孫 のネストを実現する方法が見えてない(公式のドキュメントにも何も記載がない)等, 実用上で問題となりそうな点がクリアになっていない.
細かい話でいうと、abstractなstateの扱いとかも気になるところ.
もう少し追っかけながら、頃合いを見計らって、かな。。。
まとめ
- Angular new RouterはAngular 2.xを強く意識したルーティング機構.
- Angular 2.xを見据え、Componentの概念が導入されている.
- ネーミングルール重要
- Angular 1.xユーザでも, 今から触っておくことで、Angular 2.xにアプリを移行するときに楽できる... かも?
- 画面遷移の拡張ポイントがいくつかある.
- multi-viewやPath階層とComponentの配置階層のマッピング等、未整理箇所が多数.
参考資料
- new Routerのサイト ※ ガイド中のコードサンプルはちょいちょいtypoが残っているので要注意.
- ng-japanでのBrianスライド資料