AngularJSのnew Routerを試す

2015/03/21に開催された [ng-japan](http://ngjapan.org/) にて、AngularJS New Routerの話を聞いてきたので、触りがてらのメモ.

## new Routerとは

セッションで講演されていた [Brian Fold](https://twitter.com/briantford) 曰く：

+ Angular 1.4(もうすぐリリース予定)と併せてリリースされる, Angularの新しいRouting機構
+ Angular 2.x / 1.x の両方で動作することを目標としている.
+ ngRouterだけでなく、ui-routerのように広く利用されているルーティングモジュール開発者の意見を取り入れながら開発した.

## 何はともあれ触ってみる

手っ取り早く触ってみるのであれば、npm でnew Routerをinstallしてみるのがよい.

```bash
npm install angular-new-router
cd node_modules/angular-new-router
npm install
```

`node_modules/angular-new-router`をcwdにして、WebServerを立ち上げる.

```bash
npm install -g http-server
http-server
```

これでブラウザから, [http://localhost:8080/examples/angular-1/](http://localhost:8080/examples/angular-1/) を叩けば、exampleの一覧が確認できる.

## ルーティングの基本

ここでは、`example/angular-1/animation` を例にソースコードを読んでいく.

```js:app.js
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の定義方法は一見すると奇妙だ。

```js:[参考]ui-routerを使った場合のルーティング定義
angular.modlue('app', ['ui.router']).config(function ($stateProvider) {
  $stateProvider.state('welcome', {url: '/welcome', ...});
});
```

これは、Angular 2.x を見越してのことと考えると納得できる。実際、セッションの発表でもあったのだが、先ほどのAppControler相当は、Angular 2.xでは下記のように書くことが出来る。

```ts:app.ts
@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側のコードだ.

```js:components/welcome/welcome.js
angular.module('example.welcome', []).
    controller('WelcomeController', WelcomeController);

function WelcomeController() {
  this.heading = 'Welcome to The New Angular Router Demo!';
}
```

```html:components/welcome/welcome.html
<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` でこれらのネーミングルールは変更可能なようだ。

```js:
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である:

```html: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を定義:

```js:app.js
angular.module('app', ['ngNewRouter'])
  .controller('AppController', ['$router', AppController]);

function AppController ($router) {
  $router.config([
    { path: '/',           component: 'home' },
    { path: '/detail/:id', component: 'detail' }
  ]);
}
```

`ui-sref`と同じく、遷移時に引数としてパラメータを渡せる:

```html:app.html
<a ng-link="detail({id: 5})">link to detal</a>
```

Componentからは、`$routeParams` でパラメータ値を取得出来る:

```js:components/detail/detail.js
angular.module('app.detail', ['ngNewRouter'])
  .controller('DetailController', ['$routeParams', DetailController]);

function DetailController ($routeParams) {
  this.id = $routeParams.id;
}
```

## Multi-View

先ほどの例では、`ng-viewport` が1つであったが、複数のviewportをpathに割り当てることもできる.

```app.js
$router.config([{
  path: '/home',
  components: {
    main: 'list',
    sub: 'result'
  }
}]);
```

```html
<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](https://github.com/angular/router/issues/193) で取り上げられており, v0.5.1でfixされる予定.*

*Componentをネストする際の挙動については、 [issue:117](https://github.com/angular/router/issues/117), [issue:127](https://github.com/angular/router/issues/127) で議論している最中.*

## LifeCycle Hook
new Routerはルーティング前後に処理を挟み込んだりするための拡張ポイントが用意されている. 

+ canActivate: このComponentへ遷移する直前. 「このComponentに遷移する権限があるか」のように、認可処理に用いると便利.
+ activate: このComponentへ遷移した直後. Ajaxによるデータ取得など、ある程度「重たい」処理を初期処理として挟み込むとよい.
+ canDeactivate: このComponentから遷移する直前. 終了確認等に用いるとよい.
+ deactivate: このComponentから遷移した直後.

これらの拡張ポイントへの処理追加は、Componentに上記名称のメソッドを定義することで実現する。すなわち、下記のようなコードとなる(メソッド定義に`.prototype`とか出てくる辺りが、Angular 2.xのClass記法を強く意識してるのが分かりますね).

```js
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へ遷移する場合、以下の順序でこれらの拡張ポイントが作動する。

1. (遷移開始)
1. oldComponent.canDeactivate
1. (newComponentのインスタンス作成)
1. newComponent.canActivate
1. oldComponent.deactivate
1. newComponent.activate
1. (遷移完了)

これらの拡張ポイントは、下記のいずれかを返却すると、遷移をキャンセルすることが出来る.

+ `false`
+ promiseオブジェクト(その後rejectされた場合)
+ promiseオブジェクト(その後`resolve(false)`された場合)

なお、ComponentのコンストラクタでErrorをぶん投げるような荒技を使って遷移をキャンセルすることも出来るが、「Constructorに遷移制御の処理を突っ込むな」とのこと([Guideより](https://angular.github.io/router/lifecycle))。

僕はui-router大好きっ子なので、遷移時のフックは主に下記で実現していた。

1. 遷移自体のキャンセル: `$stateChangeStart` イベントをリスンし、必要であれば、`$state.go()`実行
2. 遷移直後に必要となるデータの取得: `$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のサイト](https://angular.github.io/router/) ※ ガイド中のコードサンプルはちょいちょいtypoが残っているので要注意.
+ [ng-japanでのBrianスライド資料](https://docs.google.com/presentation/d/1iKznB6Gxx3Dw8t5zgRx0BXncR0wQuAD9Fo2HZpeeumM/pub?start=false&amp;loop=false&amp;delayms=3000&amp;slide=id.p&slide=id.p)