Angularのルーティング設定（基礎編）

> この記事は[Angular+Firebaseでチャットアプリを作る](http://qiita.com/Yamamoto0525/items/a76ea4b3924eeb82b0f9)のエントリーです。
> 前記事：[AngularのNgModuleを使って、アプリの構成を管理する](http://qiita.com/Yamamoto0525/items/c647f03a5ab56883e09e)
> 次記事：[AngularのRxJSを使ってデータの受け渡しをする](https://qiita.com/Yamamoto0525/items/efc0e5617c7427fea86a)

# この記事で行うこと

[前回の記事](http://qiita.com/Yamamoto0525/items/c647f03a5ab56883e09e)で中規模アプリのモジュール構成を扱いました。
本稿ではモジュールをベースにしたAngularのルーティングを扱っていきます。

- [ルーティングの基本設定](http://qiita.com/Yamamoto0525/items/e870713d9d05d2d36a80#%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%81%AE%E5%9F%BA%E6%9C%AC%E8%A8%AD%E5%AE%9A)
- [機能モジュールへのルーティング](http://qiita.com/Yamamoto0525/items/e870713d9d05d2d36a80#%E6%A9%9F%E8%83%BD%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%B8%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0)

### SPA（シングルページアプリケーション）のルーティング

IT関係者が「ルーティング」と聞くとネットワークのルーティングを思い浮かべてしまうかもしれませんが、ここでいうルーティングとは全くの別物です。SPAの文脈で使われる「ルーティング」とは、表示されているDOMをJavaScriptで書き換え、ページ遷移を擬似的に再現することを指します。

従来のHTMLベースの静的サイトや、Railsなどサーバーサイドで出力される動的サイトの場合、ルーティングはhttpリクエストに紐づく純粋な「ページ遷移」でした。
しかし、SPAの場合、ページ遷移にhttpリクエストが必ずしも必要ではありません。コンテンツ部分などの対象となるDOMだけを書き換え、「見た目上」のページ遷移を再現します。DOMの描画はかなり負荷の高い処理であり、その処理を大幅に削減することでSPAは高速な画面遷移を可能にしています。

ただし、この「必要なDOMだけを書き換える」という処理だけでルーティングが終わってしまうと、「URLの変更が行われない」というサービスによっては致命的な問題を引き起こします。URLの変更が行われないということは、URLをキーとして動作する解析ツールや、検索ボットなどが正常に動作しなくなるということです。
サイトの表現がリッチになっても、サイトの訪問者が減るようではSPAを採用する意味がなくなります。

最近のJavaScriptフレームワークでは、「URL変更されない」問題に対してそれぞれ独自のルーティングライブラリを使うことで対応するようになっています。Angularもその例に漏れず、パッケージの一つとして`@angular/router`を持っていますので、その実装方法を紹介していきます。

**************
（2018/1追記）記述を現時点で最新のものに差し替えました。
（2018/9追記）記述を現時点で最新のものに差し替えました。
（2020/6追記）記述を現時点で最新のものに差し替えました。
**************

# 実装内容

## ルーティングの基本設定

Angularのルーティング設定を行うにあたり、まずは今回のアプリ制作に必要なURLとそれに対応する画面を確認します。

| URL | 画面名 |
|:----|:-------|
| / | チャット画面 |
| /account/login | ログイン画面 |
| /account/sign-up | サインアップ画面 |
| /（それ以外）|４０４画面 |

`/`（ルート）にアクセスがあった場合はチャット画面に遷移し、アカウント関連のページにアクセスがあった場合はその対応する画面、それ以外の場合は404（ページが見つかりません）画面を表示するようにします。

### ルートパスへのルーティング

Angularのルーティングの設定はAppRoutingModuleで行います。
URLとコンポーネントを紐付けるルートを追記します。

```ts:src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ChatComponent } from './chat/chat.component'; // 追加

const routes: Routes = [ // 更新
  { path: '', component: ChatComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
```

次にルートコンポーネントにあった`ChatComponent`のカスタムタグを、ルーティング指定したコンポーネントを表示させる`<router-outlet></router-outlet>`に変更します。

```ts:src/app/app.component.ts
@Component({
  selector: 'app-root',
  template: ` 
  	<app-header></app-header>
  	<router-outlet></router-outlet> <!--変更-->
  `,
})
```

これでルーティングの最初の設定は終了です。
`ng serve`でサーバーを起動し、これまでと表示が変わっていないことを確認してください。

#### `Routes`と`router-outlet`

上記では、モジュールに`Routes`というパスとコンポーネントの組み合わせを示した配列を追加しました。この配列を`RouterModule`に追加することで`@angular/router`ライブラリがパスを認識し、`/`（ルート）にきたアクセスが`router-outlet`タグで指定された要素に反映されるようになります。

### 404画面へのルーティング

次に、指定されていないパスにアクセスがあった時、404画面を表示させるためのルーティング設定を行いますので、まずは404画面を作成します。

```bash
ng g component error/page-not-found
```

```html:src/app/error/page-not-found.html
<div class="page card">
	<section class="card text-center">
		<div class="card-block">
			<div class="error-code">404</div>
			<h3 class="font-bold">ページが見つかりません</h3>
		</div>
	</section>
</div>
```
```css:src/app/error/page-not-found.css
.page {
  max-width: 960px;
  height: 100%;
  margin: 0 auto;
  padding: 10px 0;
  background-color: #fff;
  border-left: 1px solid #DDDDDD;
  border-right: 1px solid #DDDDDD;
  box-shadow: 0 0 15px #DDDDDD;
  overflow: scroll;
}

.page section {
  margin: 50px 10px 30px;
}

.page section.card .error-code {
  margin: 30px 0;
  color: #2d353c; 
  font-size: 96px;
  line-height: 100px;
}
```
これで404画面の作成は完了です。
先ほど設定したルートモジュールの`Routes`に、作成した`PageNotFoundComponent`を登録します。

```ts:src/app/app.module.ts
const routes: Routes = [
  { path: '', component: ChatComponent }, 
  { path: '**', component: PageNotFoundComponent }, // 追加
];
```
今回追加したパスはワイルドカードで指定しています。
`Routes`内では上から順（配列のキーが小さい順）に評価が行われるので、この場合は次のような順序でパスが決まります。

1. パスがルート`''`と合致すれは`ChatComponent`を表示
2. パスがルート'**'と合致すれは`PageNotFoundComponent`を表示（全てのパスが対象）

#### 実行結果

![cGnXXts6V5.gif](https://qiita-image-store.s3.amazonaws.com/0/112566/3fd3bfcb-1120-8003-b418-c8db081bc2fc.gif)

#### 参考）リダイレクトさせたい場合

もし特定のページにアクセスがあった場合に、４０４画面ではなくTOPページにリダイレクトさせたいような時は、`Routes`に対し次のような記述を加えます。

```ts:src/app/app.module.ts
const routes: Routes = [
  { path: 'test', redirectTo: '', pathMatch: 'full'}, // 追加
  { path: '', component: ChatComponent }, 
  { path: '**', component: PageNotFoundComponent },
];
```
今回追加した`redirectTo`プロパティを追加することで、`/test`にきたアクセスを｀/｀ルートにリダイレクトすることができます。
この記述の最後に`pathMatch: 'full'`というプロパティを加えていますが、これは「URLが完全一致した場合」という条件を指定しているものです。これがないと`/test/aa`のようなパスにアクセスがあった場合`/aa`へとリダイレクトをしてしまいます。

## 機能モジュールへのルーティング

上記ではパスに対してコンポーネントを指定していましたが、今度は機能モジュールをパスと紐付けてみます。

### 機能モジュールの作成

機能モジュール公式で`Feature Module`と表現されています。ある特定の機能群を１つにまとめ、他のモジュールとは独立して機能するよう定義されたモジュールです。
今回はアカウント（認証）にかかるログインやサインアップといった機能を`AccountModule`として扱えるようにしていきます。

では早速、Angular CLIを使ってモジュールとコンポーネントを作成します。

```bash
ng g module account --routing --module app-routing.module
ng g component account/login
ng g component account/sign-up
```

`ng g module account --routing`の`--routing`というオプションコマンドは、ルーティング用ファイル`accont-routing.module.ts`を自動的に作成してくれます。また、`--module`オプションは宣言するモジュールの指定ができます。
設定するURLが複数あるのであれば、このコマンドを使ってルーティング用ファイル作成しておいた方が良いです。

それでは本記事の最初で定義したURLのように、`/account`にアクセスがあった場合に`AccountModule`を読み込むようルートモジュールの`Routes`で指定します。

```ts:src/app/app.module.ts

const routes: Routes = [
  {
    path: 'account',
    loadChildren: () => import('./account/account.module').then(m => m.AccountModule), // 追加
  },
  { path: '', component: ChatComponent },
  { path: '**', component: PageNotFoundComponent },
];
```
これで「`/account`にアクセス→`AccountModule`の読み込み」という動作をさせることができるようになったので、今度は`AccountModule`内でのルーティング設定を行います。

```ts:src/account/account-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { SignUpComponent } from "./sign-up/sign-up.component"; // 追加
import { LoginComponent } from "./login/login.component"; // 追加

const routes: Routes = [
  { path: 'login', component: LoginComponent }, // 追加
  { path: 'sign-up', component: SignUpComponent }, // 追加
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class AccountRoutingModule { }
```

`http://localhost:4200/account/login`、`http://localhost:4200/account/sign-up`にアクセスし、`LoginComponent`と`SignUpComponent`のテンプレートが表示されれば機能モジュールのルーティングは完了です。

> `RouterModule`のメソッドが`forRoot(appRoutes)`ではなく`forChild(appRoutes)`となっている点に注意してください。

### 他のページに遷移する

これまでは直接URLを叩くことでルーティングを確認してきましたが、今度はコンポーネント内のリンクから他の画面に遷移するための実装を行います。

まず、[前回作成したヘッダー](http://qiita.com/Yamamoto0525/items/c647f03a5ab56883e09e)からログイン画面へのリンクを有効化します。
ルーターに関するディレクティブを使えるようにするため、ルートモジュールに`RouterModule`をインポートしておきます。

```ts:src/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { environment } from '../environments/environment';
import { RouterModule } from '@angular/router'; // 追加
import { AppRoutingModule } from './app-routing.module';
import { SharedModule } from './shared/shared.module';
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { AngularFireAuthModule } from '@angular/fire/auth';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';

import { AppComponent } from './app.component';
import { ChatComponent } from './chat/chat.component';
import { HeaderComponent } from './header/header.component';
import { PageNotFoundComponent } from './error/page-not-found/page-not-found.component';

@NgModule({
  declarations: [
    AppComponent,
    ChatComponent,
    HeaderComponent,
    PageNotFoundComponent,
  ],
  imports: [
    BrowserModule,
    RouterModule, // 追加
    AppRoutingModule,
    SharedModule,
    NgbModule,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFirestoreModule,
    AngularFireAuthModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {
}

```

次に、ヘッダーのテンプレートを更新します。
ルーティングを行ったパスに遷移したい場合は、`routerLink`というディレクティブを使用します。

```html:app/header/header.component.html
<nav class="navbar fixed-top navbar-dark bg-primary">
  <a class="navbar-brand" href="#">NgChat</a>
  <span class="navbar-text">
    <a routerLink="/account/login" class="navbar-text">Login</a><!-- routerLinkに変更 -->
  </span>
</nav>
```
これでログイン画面へのリンク貼り付けが完了です。
ここでログイン画面とサインアップ画面も作成し、それぞれに行き来できるよう、`routerLink`を実装しておきます。

```html:app/account/login/login.component.html
<div class="page">
  <section class="card">
  	<form class="form-login">
  		<h2 class="form-login-heading">ログイン</h2>
	    <label for="inputEmail" class="sr-only">Email address</label>
	    <input type="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
	    <label for="inputPassword" class="sr-only">Password</label>
	    <input type="password" id="inputPassword" class="form-control" placeholder="Password" required>
	    <button class="btn btn-lg btn-info btn-block" type="submit">LOGIN</button>
	</form>
	<a routerLink="/account/sign-up" class="login-link">アカウントを作る</a>
  </section>
 </div>
```
```css:app/account/login/login.component.css
.page {
  max-width: 960px;
  height: 100%;
  margin: 0 auto;
  padding: 10px 0;
  background-color: #fff;
  border-left: 1px solid #DDDDDD;
  border-right: 1px solid #DDDDDD;
  box-shadow: 0 0 15px #DDDDDD;
  overflow: scroll;
}

.page section {
  margin: 65px 10px 30px;
}

.form-login {
  max-width: 330px;
  padding: 15px;
  margin: 0 auto;
}

.form-login .form-login-heading,
.form-login .checkbox {
  margin-bottom: 10px;
}

.form-login .checkbox {
  font-weight: normal;
}

.form-login .form-control {
  position: relative;
  height: auto;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
  padding: 10px;
  font-size: 16px;
}

.form-login .form-control:focus {
  z-index: 2;
}

.form-login #inputEmail {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}

.form-login #inputPassword {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

a.login-link {
  text-align: center;
  margin: 5px 0 15px;
}

```
```html:app/account/sign-up/sign-up.component.html
<div class="page">
  <section class="card">
  	<form class="form-signup">
  		<h2 class="form-signup-heading">アカウント作成</h2>
	    <label for="inputEmail" class="sr-only">Email address</label>
	    <input type="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
	    <label for="inputPassword" class="sr-only">Password</label>
	    <input type="password" id="inputPassword" class="form-control" placeholder="Password" required>
	    <label for="confirmationPassword" class="sr-only">Password</label>
	    <input type="password" id="confirmationPassword" class="form-control" placeholder="Password Confirmation" required>
	    <button class="btn btn-lg btn-success btn-block" type="submit">SIGN UP</button>
	</form>
	<a routerLink="/account/login"class="signup-link">ログインする</a>
  </section>
 </div>
```
```css:app/account/sign-up/sign-up.component.css
.page {
  max-width: 960px;
  height: 100%;
  margin: 0 auto;
  padding: 10px 0;
  background-color: #fff;
  border-left: 1px solid #DDDDDD;
  border-right: 1px solid #DDDDDD;
  box-shadow: 0 0 15px #DDDDDD;
  overflow: scroll;
}

.page section {
  margin: 65px 10px 30px;
}

.form-signup {
  max-width: 330px;
  padding: 15px;
  margin: 0 auto;
}

.form-signup .form-signup-heading,
.form-signup .checkbox {
  margin-bottom: 10px;
}

.form-signup .checkbox {
  font-weight: normal;
}

.form-signup .form-control {
  position: relative;
  height: auto;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
  padding: 10px;
  font-size: 16px;
}

.form-signup .form-control:focus {
  z-index: 2;
}

.form-signup #inputEmail {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}

.form-signup #inputPassword {
  border-radius: 0;
  border-collapse: collapse;
  margin-bottom: -1px;
}

.form-signup #confirmationPassword {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

a.signup-link {
  text-align: center;
  margin: 5px 0 15px;
}

```

#### 実行結果

![PK4zevaCpH.gif](https://qiita-image-store.s3.amazonaws.com/0/112566/03fa8346-bc8f-3d6f-410c-c021d85ebe9d.gif)

これで認証画面への遷移をすることができるようになりました。
次はAngularで採用されているRxJSについて解説します。

## ソースコード

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