この記事はAngular+Firebaseでチャットアプリを作るのエントリーです。
前記事:AngularのNgModuleを使って、アプリの構成を管理する
次記事:AngularのRxJSを使ってデータの受け渡しをする
この記事で行うこと
前回の記事で中規模アプリのモジュール構成を扱いました。
本稿ではモジュールをベースにしたAngularのルーティングを扱っていきます。
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画面 |
/
(ルート)にアクセスがあった場合はチャット画面に遷移し、アカウント関連のページにアクセスがあった場合はその対応する画面、それ以外の場合は404(ページが見つかりません)画面を表示するようにします。
ルートパスへのルーティング
Angularのルーティングの設定はAppRoutingModuleで行います。
URLとコンポーネントを紐付けるルートを追記します。
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>
に変更します。
@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画面を作成します。
ng g component error/page-not-found
<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>
.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
を登録します。
const routes: Routes = [
{ path: '', component: ChatComponent },
{ path: '**', component: PageNotFoundComponent }, // 追加
];
今回追加したパスはワイルドカードで指定しています。
Routes
内では上から順(配列のキーが小さい順)に評価が行われるので、この場合は次のような順序でパスが決まります。
- パスがルート
''
と合致すれはChatComponent
を表示 - パスがルート'**'と合致すれは
PageNotFoundComponent
を表示(全てのパスが対象)
実行結果
参考)リダイレクトさせたい場合
もし特定のページにアクセスがあった場合に、404画面ではなくTOPページにリダイレクトさせたいような時は、Routes
に対し次のような記述を加えます。
const routes: Routes = [
{ path: 'test', redirectTo: '', pathMatch: 'full'}, // 追加
{ path: '', component: ChatComponent },
{ path: '**', component: PageNotFoundComponent },
];
今回追加したredirectTo
プロパティを追加することで、/test
にきたアクセスを`/`ルートにリダイレクトすることができます。
この記述の最後にpathMatch: 'full'
というプロパティを加えていますが、これは「URLが完全一致した場合」という条件を指定しているものです。これがないと/test/aa
のようなパスにアクセスがあった場合/aa
へとリダイレクトをしてしまいます。
機能モジュールへのルーティング
上記ではパスに対してコンポーネントを指定していましたが、今度は機能モジュールをパスと紐付けてみます。
機能モジュールの作成
機能モジュール公式でFeature Module
と表現されています。ある特定の機能群を1つにまとめ、他のモジュールとは独立して機能するよう定義されたモジュールです。
今回はアカウント(認証)にかかるログインやサインアップといった機能をAccountModule
として扱えるようにしていきます。
では早速、Angular CLIを使ってモジュールとコンポーネントを作成します。
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
で指定します。
const routes: Routes = [
{
path: 'account',
loadChildren: () => import('./account/account.module').then(m => m.AccountModule), // 追加
},
{ path: '', component: ChatComponent },
{ path: '**', component: PageNotFoundComponent },
];
これで「/account
にアクセス→AccountModule
の読み込み」という動作をさせることができるようになったので、今度はAccountModule
内でのルーティング設定を行います。
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を叩くことでルーティングを確認してきましたが、今度はコンポーネント内のリンクから他の画面に遷移するための実装を行います。
まず、前回作成したヘッダーからログイン画面へのリンクを有効化します。
ルーターに関するディレクティブを使えるようにするため、ルートモジュールにRouterModule
をインポートしておきます。
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
というディレクティブを使用します。
<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
を実装しておきます。
<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>
.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;
}
<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>
.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;
}
実行結果
これで認証画面への遷移をすることができるようになりました。
次はAngularで採用されているRxJSについて解説します。
ソースコード
この時点でのソースコード
※firebaseのapiKeyは削除しているので、試すときは自身で作成したapiKeyを入れてください。