前提
Angular2 ではコンポーネントへのアクセス制御を @CanActivate
アノテーションで行います。
@Component({
selector: 'some-component',
template: '<div>...</div>',
})
@CanActivate((next, prev): Promise<boolean> | boolean => {
return true;
})
export class SomeComponent {
}
@CanActivate
へはアクセス可否を boolean
または Promise<boolean>
で返す関数を渡します。
課題
ところで、認証状況(セッション)は SessionService
のようなサービスを使って管理することが多いかと思います。
export class SessionService {
isActive(): boolean {
// 認証状況を判定して真偽値を返す何らかの処理
}
}
さて、これを @CanActivate
から使おうとすると、まぁ、使えないわけです。
@Component({
selector: 'some-component',
template: '<div>...</div>',
})
@CanActivate(() => {
// クラスの外側なので `this` は参照できないので下記はエラーとなる。
return this._sessionService.isActive();
})
export class SomeComponent {
constructor(private _sessionService: SessionService) {
}
}
さて、困った。
解決編、その前に
例えば SessionService
を new
でインスタンス化して使えばいいんじゃないか、と思うかもしれません。しかし、それは次のような理由で避けるべきです。
- (その是非は別として)
SessionService
が状態を持っており、シングルトンであることを期待している場合がある。 -
SessionService
が別のサービス(例えばHttp
)などに依存している場合がある。
そんなわけで、なんとかして判定ロジックに SessionService
のインスタンスを DI してやる、というのが目指すべき方向性です。
Angular2 の DI
Angular2 の DI についての基本的な説明は @laco0416 さんが昨年末のアドベントカレンダーに投稿した Angular2のDIを知る を読んでいただくのが一番かと思います。
実際に Angular2 における DI を担っているのは Injector
というクラスです。コンポーネントを生成する場合、必ずこの Injector
もセットで生成され、コンポーネントの親子構造と同じ Injector
の親子構造を持ちます。注入するサービスを親コンポーネントを辿って見つける仕組みは、この Injector
の親子構造によって実現されています。
ということはなんとかして Injector
のインスタンスをどこかから持ってくることができれば、目的が達成できそうです。
ComponentRef
ところで bootstrap(App, PROVIDERS)
の戻り値は Promise<ComponentRef>
です。実はこの ComponentRef
は次のような情報を持っています。
プロパティ | 内容 |
---|---|
location: ElementRef |
対象のHTML要素 |
instance: any |
コンポーネントのインスタンス |
componentType: Type |
コンポーネントの型(コンストラクタ) |
injector: Injector |
Injector のインスタンス(!) |
つまり……
let app = bootstrap(App, PROVIDERS);
export const injector: Promise<Injector> = app.then(ref => ref.injector);
とすれば Promise<Injector>
が取得できそうです。正解にかなり近づきました気がしますが、上記のコードのままでは最上位層であるはずの app.ts
に各コンポーネントが依存してしまいます。
解決編
ここまでのまとめ。
-
@CanActivate
からはコンポーネントに注入される各サービスにアクセスできない。 - Angular2 の DI は
Injector
が行っている。 -
Injector
のインスタンスはComponentRef
のインスタンスから取得できる。 -
ComponentRef
のインスタンスはbootstrap
関数の戻り値から取得できる。 -
bootstrap
の戻り値をそのまま公開(export
)すると依存関係がおかしいことになる。
そこで次のように Deferred
を導入することで解決します。
export class SessionService {
isActive(): boolean {
// 認証状況を判定して真偽値を返す何らかの処理
}
}
import {Injector} from "angular2/core";
import {Deferred} from "ts-deferred";
export const deferredInjector = new Deferred<Injector>();
export const AppInjector: Promise<Injector> = deferredInjector.promise;
import {AppInjector} from "./app-injector";
import {SessionService} from "./session.service";
export function sessionIsActive(): Promise<boolean> {
return AppInjector
.then(injector => injector.get(SessionService))
.then(sessionService => sessionService.isActive());
}
import {Component} from "angular2/core";
import {CanActivate} from "angular2/router";
import {sessionIsActive} from "./functions";
import {SessionService} from "./session.service";
@Component({
selector: 'some-component',
template: '<div>...</div>',
})
@CanActivate(sessionIsActive)
export class SomeComponent {
}
import {Component, ComponentRef} from "angular2/core";
import {bootstrap} from "angular2/platform/browser";
import {RouteConfig, ROUTER_DIRECTIVES} from "angular2/router";
import {deferredInjector} from "./app-injector";
@Component({
template: '<router-outlet></router-outlet>',
directives: [ROUTER_DIRECTIVES],
})
@RouteConfig({
// ルート定義
})
class App {
}
let componentRef = bootstrap(App, [SessionService]);
componentRef.then(ref => deferredInjector.resolve(ref.injector));
上記例では Deferred
の実装として拙作の ts-deferred を利用していますが、別の実装を利用しても問題ありません。
最後に
もっといい解決方法をご存じの方は、ぜひコメントで教えてください!