前提
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 を利用していますが、別の実装を利用しても問題ありません。
最後に
もっといい解決方法をご存じの方は、ぜひコメントで教えてください!