はじめに
大規模なSPAでは必要不可欠なルーティングの機能をAngular2で試してみます。
Angular2の2.0.0-beta.0を使います。
Router
まずは最小限の例を示しましょう。
beta.0で更新された5 MIN QUICKSTARTを参考にディレクトリ構成は以下のようにしています。
my-app
├ node_modules
├ app
│ ├ app.component.ts
│ ├ page1.component.ts
│ ├ page2.component.ts
│ └ boot.ts
├ index.html
├ package.json
└ tsconfig.json
index.htmlの中身は以下のとおりです。
<html>
<head>
<title>Angular 2 QuickStart</title>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
<script src="node_modules/angular2/bundles/router.dev.js"></script>
<script>
System.config({
packages: {
app: {
format: 'register',
defaultExtension: 'js'
}
}
});
System.import('app/boot')
.then(null, console.error.bind(console));
</script>
<base href="/">
</head>
<body>
<my-app>Loading...</my-app>
</body>
</html>
5 MIN QUICKSTARTのindex.htmlに、router.dev.jsを読み込むscript要素とbase要素を追加しています。
4つのTypeScriptファイルの中身は以下のようになっています。
import {Component} from 'angular2/core'
const template = `
<h2>page 1</h2>
`;
@Component({
template: template
})
export class Page1 {
}
import {Component} from 'angular2/core'
const template = `
<h2>page 2</h2>
`;
@Component({
template: template
})
export class Page2 {
}
Page1とPage2はtemplateから直接参照しないので、selectorは省略してもよさそうでした。
import {Component} from 'angular2/core'
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router'
import {Page1} from './page1.component'
import {Page2} from './page2.component'
const template = `
<router-outlet></router-outlet>
`;
@Component({
selector: 'my-app',
template: template,
directives: [ROUTER_DIRECTIVES]
})
@RouteConfig([
{
useAsDefault: true,
path: '/page1',
component: Page1
},
{
path: '/page2',
component: Page2
}
])
export class AppComponent {
}
import {bootstrap} from 'angular2/platform/browser'
import {ROUTER_PROVIDERS} from 'angular2/router'
import {AppComponent} from './app.component'
bootstrap(AppComponent, [
ROUTER_PROVIDERS
]);
ブラウザで開くとhttp://localhost:3000/page1に遷移し「page 1」と表示されているはずです。
URLを入力しhttp://localhost:3000/page2に遷移すると表示内容が「page 2」に変わるかと思います。
とりあえず大事なのは、app.component.tsの@RouteConfigです。
引数にはRouteの配列を与えます。
Routeの引数に必須なのはpathとcomponentで、pathはURLに反映されるpathの名前、componentはこのRouteに対応するComponentです。
Page1へのRouteにuseAsDefault: trueを与えることで、http://localhost:3000/にアクセスしたときにhttp://localhost:3000/page1に遷移するようになっています。
Page1、Page2の内容は、templateの<router-outlet></router-outlet>の箇所に表示されます。
<router-outlet></router-outlet>を使うには、angular/routerからimportするROUTER_DIRECTIVESが必要です。
routerLink
さて、上のままではURLを直接打ち込まないとページ遷移ができないので不便です。
routerLinkを使ってRouteへのリンクを作ってみます。
import {Component} from 'angular2/core'
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router'
import {Page1} from './page1.component'
import {Page2} from './page2.component'
const template = `
<a [routerLink]="['Spam']">page1</a>
<a [routerLink]="['Ham']">page2</a>
<router-outlet></router-outlet>
`;
@Component({
selector: 'my-app',
template: template,
directives: [ROUTER_DIRECTIVES]
})
@RouteConfig([
{
useAsDefault: true,
path: '/page1',
component: Page1,
name: 'Spam'
},
{
path: '/page2',
component: Page2,
name: 'Ham'
}
])
export class AppComponent {
}
変更点は2箇所です。
まずRouteのパラメータにnameを与えます。
次にtemplateにa要素を追加し、属性に[routerLink]="['Spam']"のように書きます。
ここでの「Spam」が、Routeのnameの「Spam」に対応します。
また、alpha版の資料だとrouter-linkと書かれていたりしますが、routerLinkに変更になっているので気をつけてください。
router.navigate
formをsubmitされたときなど、Component内で処理を行ってからページ遷移を行いたい場合があると思います。
実装例は以下のとおりです。
import {Component} from 'angular2/core'
import {Router} from 'angular2/router'
const template = `
<h2>page 2</h2>
<button (click)="goPage1()">go to page1</button>
`;
@Component({
template: template
})
export class Page2 {
constructor(public router: Router) {
}
goPage1() {
console.log('go to page1');
this.router.navigate(['Spam']);
}
}
router.navigateの引数はtemplateでのrouterLinkと同じなようです。
LocationStrategy
Angular2ではPathをURLにするときに、実際にhttps://example.com/page1とhttps://example.com/#/page1のどちらにするかをLocationStrategyを使って設定できます。
Angular1の$locationProvider.html5Modeと同じような感じです。
前者はPathLocationStrategy、後者はHashLocationStrategyと呼びます。
デフォルトではPathLocationStrategyなので、HashLocationStrategyに変更する例は以下のようになります。
import {provide} from 'angular2/core'
import {bootstrap} from 'angular2/platform/browser'
import {ROUTER_PROVIDERS, LocationStrategy, HashLocationStrategy} from 'angular2/router'
import {AppComponent} from './app.component'
bootstrap(AppComponent, [
ROUTER_PROVIDERS,
provide(LocationStrategy, {useClass: HashLocationStrategy})
]);
Lifecycle Hooks
Angular2ではLifecycle HooksによってComponentの状態変更のタイミングで処理をはさむことができます。
RouterのLifecycle HookはOnActivate、OnReuse、OnDeactivate、CanReuse、CanDeactivateの5種類があります。
各HookはInterfaceとして提供されており、ComponentでInterfaceのメソッドを実装することになります。
以下は簡単な例です。
import {Component} from 'angular2/core'
import {OnActivate, OnReuse, OnDeactivate, CanReuse, CanDeactivate} from 'angular2/router'
const template = `
<h2>page 1</h2>
`;
@Component({
template: template
})
export class Page1 implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanDeactivate {
routerOnActivate() {
console.log('onActivate');
}
routerOnReuse() {
console.log('onReuse');
}
routerOnDeactivate() {
console.log('onDeactivate');
}
routerCanReuse() {
return confirm('can reuse?');
}
routerCanDeactivate() {
return confirm('can deactivate?');
}
}
OnActivateのInterfaceにrouterOnActivateメソッドが対応するという感じになっています。
routerCanReuseとrouterCanDeactivateは真偽値またはPromiseを返すようになっています。
今回は試していませんが、Promiseを返すことで非同期的な処理も行うことができます。
おわりに
本稿では、Angular2のRouterでよく使いそうな機能などを試してみました。
今年の春頃にAngular1向けのComponent Routerを触ってみましたが、そこから更に洗練された印象を受けます。
本稿を書くにあたって、@ovrmrw さんがRouterを使うサンプルを既に書いてくれていたのでとても助かりました。
Routerの詳細は公式のドキュメントも目を通してみてください。