LoginSignup
51
47

More than 3 years have passed since last update.

Angularのルーティング設定(応用編)

Last updated at Posted at 2018-09-26

この記事はAngular+Firebaseでチャットアプリを作るのエントリーです。
前記事:Angular+Firebase Authenticationで認証機能を導入する
次記事:Angular+Firebase ユーザー設計とデータの保護

この記事で行うこと

前回の記事ではFirebaseの認証機能導入しました。
本稿ではサイトランディング時の認証照会とガード(Guard)を使ったページの保護を扱っていきます。

ガード(Guard)とは

近年のWEBアプリケーションは、ほとんどと言っていいほど認証機能を保有しています。
認証を通過していないユーザーに見せたくないページがある場合は、そのページを読み込む前に認証についてのチェックを行い、そのページの読み込みを中止するか、他のページへ誘導する必要があります。

ガード(Guard)は、そのページ(ルート)に対しアクセスがあった場合、対象となるコンポーネントを読み込む前、もしくは後に動作するサービスです。
ガードの種類は次の5つになっています。

ガード名 読み込むタイミング 使用例
CanActivate モジュールを読込み始めた後、コンポーネントを読込む前 認証の確認
CanActivateChild モジュールを読込み始めた後、コンポーネントを読込む前 CanActivateされたルートの子ルートへの認証の確認
CanDeactivate コンポーネントを離れた後、次のルートを読込む前 入力したフォームの削除確認
Resolve モジュールを読込み始めた後、コンポーネントを読み込む前 前のルートで指定したdataの読み込み
CanLoad モジュールを読込む前 ローディングバーの起動

この記事ではCanActivateを使って、チャット画面の認証状況を確認します。


(追記:2020/6)現時点(2020年6月)での最新の内容に書き換えています。


実装内容

サイトランディング時に認証の確認をする

まず、認証を通過している場合、通過していない場合のそれぞれで、ルータがどのような挙動をするのか定義していきます。

認証を通過している場合

アクセス先 表示される画面
チャット画面 チャット画面
アカウント画面(login) チャット画面
アカウント画面(signup) チャット画面

認証を通過していない場合

アクセス先 表示される画面
チャット画面 アカウント画面(login)
アカウント画面(login) アカウント画面(login)
アカウント画面(signup) アカウント画面(signup)

認証状況の判定

挙動についての把握ができたので、次はSessionServiceにログイン状況を判断するメソッドを追加し、ルートコンポーネントで読み込むようにします。

src/service/session.service.ts

  // ログイン状況確認
  checkLogin(): void { // 追加
    this.afAuth
      .authState
      .subscribe(auth => {
        // ログイン状態を返り値の有無で判断
        this.session.login = (!!auth);
        this.sessionSubject.next(this.session);
      });
  }
src/app.component.ts
import { SessionService } from './service/session.service'; // 追加

// ~~~ 省略 ~~~

  constructor(private session: SessionService) { // 追加
    this.session. checkLogin();
  }

}

this.afAuth.authStateはangularfireのメソッドです。この返り値の有無でログイン状況の判断ができます。

ルートコンポーネントはどのページにランディングしても読み込まれるコンポーネントなので、サイトランディング時の認証確認はここで行います。

チャット画面を保護する

authガードの作成

サイトランディング時の認証確認が整ったので、次にガードを使ったページの保護を行います。
まず、コマンドラインで認証用のガードを作成します。

ng g guard guard/auth

作成したガードをAppRoutingModuleに登録します。

src/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ChatComponent } from './chat/chat.component';
import { PageNotFoundComponent } from './error/page-not-found/page-not-found.component';
import { AccountModule } from './account/account.module';
import { AuthGuard } from './guard/auth.guard'; // 追加

const routes: Routes = [
  {
    path: 'account',
    loadChildren: () => import('./account/account.module').then(m => m.AccountModule),
  },
  {
    path: '',
    component: ChatComponent,
    canActivate: [AuthGuard],  // 追加
  },
  {
    path: '**',
    component: PageNotFoundComponent,
  },
];

これでチャット画面を開く前に、AuthGuardが読み込まれるようになりました。
次に作成されたAuthGuardの内容を確認します。

src/guard/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return true;
  }

}

自動作成された時点で、CanActivateが設定されています。
ここではnext(遷移先の情報)とstate(遷移元の情報)が引数に指定されていますが、これらはURLの判定や、リダイレクトする場合に必要となります。

また、返り値にObservavle、Promise、booleanの3つが指定されていますが、返り値としてtrueであれば遷移、falseであれば遷移しないと判断してください。

これを踏まえた上で、AuthGuardの実装を行います。

src/guard/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';  // 変更
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; // 更新

import { SessionService } from '../service/session.service';  // 追加

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  constructor(private session: SessionService,  // 追加
              private router: Router) {
  }

  canActivate(next: ActivatedRouteSnapshot,
              state: RouterStateSnapshot): Observable<boolean> {
    return this.session // 変更
      .checkLoginState()
      .pipe(
        map(session => {
          // ログインしていない場合はログイン画面に遷移
          if (!session.login) {
            this.router.navigate([ '/account/login' ]);
          }
          return session.login;
        })
      );
  }

}
src/service/session.service.ts
import { Observable, Subject } from 'rxjs'; // 更新
import { map } from 'rxjs/operators'; // 更新

// ~~~ 省略 ~~~

  // ログイン状況確認(State)
  checkLoginState(): Observable<Session> { // 追加
    return this.afAuth
      .authState
      .pipe(
        map(auth => {
          // ログイン状態を返り値の有無で判断
          this.session.login = (!!auth);
          return this.session;
        })
      )
  }

session.service.tsにログイン状況確認用のStateを返すcheckLoginState()を追加しました。canActivateの返り値はObserbableを選択し、loginの値をmapオペレータで返しています。
これによって、チャット画面遷移時にログインしている場合は通過、ログインしていない場合はログイン画面にリダイレクトされるようになりました。

コード内からのルーティング

ルーティング(基礎編)ではテンプレート上で遷移する場合の記述を紹介しましたが、ここではコードの中で遷移させるnavigateメソッドを使用しています。
このメソッドを使用したい時は、上記のコードのように@angular/routerからRouterをインポートしてDIしておく必要があります。

ここでは/account/loginのように絶対パスを使って指定していますが、もし相対パスを使用したい場合はActivatedRouteをDIして下記のように記述します。

import { Router, ActivatedRoute } from '@angular/router';

// ~~~ 省略 ~~~

  constructor(private router: Router,
          private route: ActivatedRoute) {
  }

// signupから遷移したい場合
this.router.navigate(['../login'], { relativeTo: this.route });

Observableのオペレータ

ここで初めてmapというObservableのオペレータが登場しました。
オペレータはストリームに流れてきた値に対し、subscribeする前に処理を加えてくれるメソッドです。

オペレータを使えばsubscribe時にデータ取得タイミングやフィルタリング、データ結合といった処理を指定するなど、データの状態管理を容易にすることができます。

具体的なオペレータの種類、及び使用シーンについては登場する都度説明していきますが、取り急ぎ綺麗にまとめてある記事を参考として紹介しておきます。

参考:RxJSのOperatorsのコード まとめ

アカウント画面をスキップする

さて、次はログイン前のアカウント画面にガードを適用します。
ここで期待される挙動は、「ログイン前→アクセスするとそのまま表示」「ログイン後→アクセスするとログイン後画面(チャット)に遷移」というものになります。

loginガードの作成

authガードと同様、コマンドラインで認証用のガードを作成します。

ng g guard guard/login

作成したガードをルートモジュールに登録し、authガードと同様にloginガードを実装します。

src/app-routing.module.ts
import { AuthGuard } from './guard/auth.guard'; // 追加
import { LoginGuard } from './guard/login.guard'; // 追加

const routes: Routes = [
  {
    path: 'account',
    loadChildren: () => import('./account/account.module').then(m => m.AccountModule),
    canActivate: [LoginGuard],  // 追加
  },
  {
    path: '',
    component: ChatComponent,
    canActivate: [AuthGuard],  // 追加
  },
  {
    path: '**',
    component: PageNotFoundComponent,
  },
];
src/guard/login.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; // 更新
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; // 更新

import { SessionService } from '../service/session.service';  // 追加

@Injectable({
  providedIn: 'root'
})
export class LoginGuard implements CanActivate {

  constructor(private session: SessionService,  // 追加
              private router: Router) {
  }

  canActivate(next: ActivatedRouteSnapshot,
              state: RouterStateSnapshot): Observable<boolean> {
    return this.session // 変更
      .checkLoginState()
      .pipe(
        map(session => {
          // ログインしていない場合はログイン画面に遷移
          if (session.login) {
            this.router.navigate(['/']);
          }
          return !session.login;
        })
      );
  }

}

これでログイン時はアカウント画面がスキップされるようになりました。

CanLoadの返り値をObservableにする場合

今回の実装では、AccountModuleに対してCanActivateを適用しています。この場合、LoadGuardが適用されるのはAccountModuleを読み込んだ後になり、もしデータを読み込ませる前にリダイレクトしたいような場合にはCanLoadを使う必要があります。

ただしCanLoadの返り値をObservableで利用しようとした場合は、CanActivateと扱いが違うため注意が必要です。

canActivate, canDeactive and canActivateChild will only check the first value emitted and won't wait for observables to complete.
canLoad will check all the values returned and wait until all observables complete.
訳:canActivate、canDeactiveおよびcanActivateChildは、最初に放出された値のみをチェックし、observableが完了するのを待つことはありません。
canLoadは返されたすべての値をチェックし、すべてのobservableが完了するまで待機します。
参考:https://github.com/angular/angular/issues/18991

つまり、ただmapオペレータを配置しているだけではCanLoadは動かず、take(1)などのオペレータを使って、observableの利用回数を制限する必要があります。

src/core/guard/login.guard.ts

  // このコードでは動作しない
  canLoad(route: Route): Observable<boolean> {
    return this.session
      .checkLoginState()
      .pipe(
        map( session => {
          return !session.login;
        })
      )
  }

  // このコードだと動作する
  canLoad(route: Route): Observable<boolean> {
    return this.session
      .checkLoginState()
      .pipe(
        take(1),
        map( session => {
          return !session.login;
        })
      )
  }

}

これでルーティングにかかる基本的な実装は完了しました。
次はユーザーデータの設計とデータの保護について解説します。

ソースコード

この時点でのソースコード
※firebaseのapiKeyは削除しているので、試すときは自身で作成したapiKeyを入れてください。

51
47
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
47