Angularのルーティング設定（応用編）

> この記事は[Angular+Firebaseでチャットアプリを作る](http://qiita.com/Yamamoto0525/items/a76ea4b3924eeb82b0f9)のエントリーです。
> 前記事：[Angular+Firebase Authenticationで認証機能を導入する](https://qiita.com/Yamamoto0525/items/054b28e8b34135399237)
> 次記事：[Angular+Firebase ユーザー設計とデータの保護](https://qiita.com/Yamamoto0525/items/e94fc6da25b2d2524332)

# この記事で行うこと

[前回の記事](https://qiita.com/Yamamoto0525/items/054b28e8b34135399237)ではFirebaseの認証機能導入しました。
本稿ではサイトランディング時の認証照会とガード（Guard）を使ったページの保護を扱っていきます。

- [サイトランディング時に認証の確認をする](#サイトランディング時に認証の確認をする)
- [チャット画面を保護する](#チャット画面を保護する)
- [アカウント画面をスキップする](#アカウント画面をスキップする)

### ガード（Guard）とは

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

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

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

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

*****
（追記：2020/6）現時点（2020年6月）での最新の内容に書き換えています。
*****

# 実装内容

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

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

**認証を通過している場合**

|アクセス先|表示される画面|
|:-----------|:-----------|
|チャット画面|チャット画面|
|アカウント画面（login）|チャット画面|
|アカウント画面（signup）|チャット画面|

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

|アクセス先|表示される画面|
|:-----------|:-----------|
|チャット画面|アカウント画面（login）|
|アカウント画面（login）|アカウント画面（login）|
|アカウント画面（signup）|アカウント画面（signup）|

### 認証状況の判定

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

```ts:src/service/session.service.ts

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

// ~~~ 省略 ~~~

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

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

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

## チャット画面を保護する

### authガードの作成

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

```bash
ng g guard guard/auth
```
作成したガードをAppRoutingModuleに登録します。

```ts: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`の内容を確認します。

```ts: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<boolean>、Promise<boolean>、booleanの３つが指定されていますが、返り値として`true`であれば遷移、`false`であれば遷移しないと判断してください。

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

```ts: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;
        })
      );
  }

}
```
```ts: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して下記のように記述します。

```ts
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のコード まとめ](https://qiita.com/gupuru/items/4d692349e6efc7354587)

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

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

### loginガードの作成

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

```bash
ng g guard guard/login
```

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

```ts: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,
  },
];
```
```ts: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の利用回数を制限する必要があります。

```ts: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;
        })
      )
  }

}
```

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

## ソースコード

この時点での[ソースコード](https://github.com/Yamamoto0525/NgChat/tree/b07f220c06a1281ca1e2c35d9ff981b2a5dd11ed)
※firebaseのapiKeyは削除しているので、試すときは自身で作成したapiKeyを入れてください。