Angular&Firebase を使ってがっつりサーバーレスな Web サービスを作ってみた

# :one: はじめに

「Angular」と「Firebase」を使って**サーバーレス**なWEBサービスの開発・運用したノウハウを紹介します。
この記事では詳細な技術的なことにはあまり触れず、「**どんな技術を使用してWEBサービスを作ろうか**」を検討している人向けに記載しています。
みなさんのサービス開発時の検討材料になれば幸いです。

## Angularとは
Googleによって開発が進められているJavaScript(TypeScript)フレームワークです。SPA(シングルページアプリケーション)が容易に作成できます。

## Firebaseとは
Googleによって開発が進められているWEBサービスやモバイルアプリに必要なサーバー処理を提供するmBaasサービスです。

# :two: 要件
以下の要件を満たせるものを目指しました。

| No. | 要件                       | 実現方法              |
|-----|:---------------------------|:---------------------|
|   1 | WEB上でアプリ並みの操作性を実現したい | Angularでシングルページアプリケーション(SPA)を作成<br>Angular/Materialで各種コンポーネント部品を利用 |
|   2 | PC/タブレット/スマホに対応したい | angular/flex-layoutを使用してレスポンシブデザインに対応 |
|   3 | フロントエンド開発に専念して、バックエンド開発をしたくない | バックエンド開発の代わりにFirebaseを使ったmBaasサービスを利用 |
|   4 | WEBサービスとモバイルアプリを1つのソースコードで管理したい | PhoneGapやCordovaを使ってWEBアプリをAndroid/iOS向けにアプリを作成 |

# :three: 環境

## 開発環境
開発に使用した各種ツールや環境です。メジャーで安定した開発が続けられるものを基準に採用しています。

| No. | 項目               | Ver.    | 備考              |
|-----|:-------------------|:--------|:---------------------|
|   1 | Visual Studio Code | 1.17.2  | Microsoft製のテキストエディター |
|   2 | Node.js            | 6.11.3  | JavaScript開発環境 |
|   3 | npm                | 3.10.10 | Node.jsのパッケージ(ライブラリ)管理マネージャー |
|   4 | TypeScript         | 2.5.3   | Microsoft製のオープンソースのプログラミング言語 |
|   5 | angular-cli        | 1.4     | Google製のangularを爆速で開発するためのツール |

※ 開発環境のOSはWindowsですが、macOSやLinuxでも同じものが使用できます。

## 要素技術(ライブラリ)

作成したWEBサービスに利用した要素技術(ライブラリ)の一覧です。こちらもメジャーで安定した開発が続けられるものを基準に採用しています。現時点でほぼ最新のバージョンを使用しています。

| No. | 項目    | Ver. | 備考            |
|-----|:--------------------|:-----|:-------------------|
|   1 | angular             | 4.4         | Googleによって開発が進められているJavaScript(TypeScript)フレームワーク |
|   2 | angular/material    | 2.0.0-beta.12 | Angular用のMaterialデザインのライブラリ |
|   3 | angular/flex-layout | 2.0.0-beta.9  | Angular用のレイアウトのライブラリ |
|   4 | firebase            | 4.6         | Googleによって開発が進められているWEBサービスやモバイルアプリに必要なサーバー処理を提供するmBaasサービスを利用するためのライブラリ |
|   5 | marked              | 0.3           | Markdown文字列をHTMLに変換するライブラリ |
|   6 | moment              | 2.18          | 日時処理を便利にしてくれるライブラリ |

※ betaバージョンが入っていますので突然の仕様変更や不具合が存在する可能性があります。
※ 現在のWEB環境はリリース版となっていても脆弱性の修正や仕様変更などはつきものなため、個人的にはbeta版であろうと開発が活発でオープンなものであれば気にしていません。

## 運用環境
| No. | 項目                        | 備考                                                          |
|-----|:----------------------------|:-------------------------------------------------------------|
|   1 | Firebase Hosting         | いわゆるWEBサーバー。HTML/CSS/JavaScriptなどの静的ファイルを設置 |
|   2 | Firebase Authentication  | ユーザー認証に関する機能<br>メールアドレスやGoogleアカウントによる認証 |
|   3 | Firebase Storage         | 画像などのユーザーファイルをアップロードして管理する                 |
|   4 | Firebase Cloud Firestore | 記事データなど動的に作成されるデータ類を保持する<br />なお同様の目的を果たせるものとして「Firebase Realtime Database」がありますが、これからはじめる方はFirestoreをお勧めします。 |
|   5 | Firebase Cloud Functions | サーバー側で行いたい処理がある場合にJavaScriptの関数をサーバー側に設置可能<br>例：[ユーザーからの支払い処理](https://github.com/firebase/functions-samples/tree/master/stripe)など |
|   6 | Google Search Console       | 検索エンジンへの対応                                           |
|   7 | Google Analytics            | アクセス解析 |

# :four: プチノウハウ

## 検索エンジン対応1
検索エンジンはキーワードと対応するページを紐づけます。SPAではシングルページという名の通りページが1つとなるため検索エンジンとの相性はよくありません。
そこで、angularのrouter機能を使いURLと表示する内容を連動させることでこの問題を解決します。

```ts:app-routing.module.ts｜ルーティング設定例
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';
import { FeedbackComponent } from './pages/feedback/feedback.component';
import { WatchComponent } from './pages/watch/watch.component';
import { MySettingsComponent } from './pages/my-settings/my-settings.component';
import { MyArticlesComponent } from './pages/my-articles/my-articles.component';
import { PostComponent } from './pages/post/post.component';

const routes: Routes = [
    {
        path: '',
        component: HomeComponent,
    },
    {
        path: 'feedback',
        component: FeedbackComponent,
    },
    {
        path: 'watch/:id',
        component: WatchComponent,
    },
    {
        path: 'my/settings',
        component: MySettingsComponent,
    },
    {
        path: 'my/articles',
        component: MyArticlesComponent,
    },
    {
        path: 'my/post',
        component: PostComponent,
    },
    {
        path: 'my/edit/:id',
        component: PostComponent,
    },
];

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

## 検索エンジン対応2
昔のクローラー(検索エンジン)はSPAなどのJavaScriptなどで動的にDOMが書き換わった文章を読めませんでしたが、現在のGoogleのクローラーはある程度は動的にDOMが書き換わっても対応できるようです。

しかし、現在のクローラーでも最新のJavaScript規格に対応していないようですのでshimを入れています。

```html:index.html｜headに追加
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.4.1/shim.min.js"></script>
```

## 検索エンジン対応3 - (未対応)
```Angular Universal``` というSSR(サーバーサイドレンダリング)によって、SPA(シングルページアプリケーション)をサーバー上で実行しDOMを生成して、クライアントには完成したDOMを返す技術があります。
クライアント(クローラー)側で動的にDOMが書き換わらないため、検索エンジンにとっては最も好ましいといえます。

ただ、私の環境ではrxjsのObservable.fromPromiseでエラーが発生してしまう問題を解決できなかったため現在は未対応の状態です。

※ 「検索エンジン対応1,2」のみで、Googleのクローラーからはインデックスされていますので、Angular-Universalによるサーバーサイドレンダリングの対応は**とりあえず不要**と言えそうです。

## Google Analytics 対応
コンテンツが動的に読み込まれてアドレスバーの URL が更新された時点で、トラッカーに保存されているデータも更新する必要があります。
https://developers.google.com/analytics/devguides/collection/analyticsjs/single-page-applications?hl=ja
https://qiita.com/Akira-Isegawa/items/845aee44d79197e9844f



```bash:コマンド｜TypeScript用の定義ファイルを追加
npm install --save-dev @types/google.analytics
```

``` html:index.html｜headに追加
    <!-- Google Analytics -->
    <script src="https://www.googletagmanager.com/gtag/js?id=UA-*********-1"></script>
</head>
```

``` ts:app-routing.module.ts
import { } from 'google.analytics';

// Google Analytics の初期化コード
window['dataLayer'] = window['dataLayer'] || [];
function gtag(arg1, arg2) { window['dataLayer'].push(arguments); }
gtag('js', new Date());
gtag('config', 'UA-*********-1');
```

``` ts:app-routing.module.ts
export class AppRoutingModule {
    /**
     * DIコンストラクタ
     */
    public constructor(router: Router) {

        // ルーターイベントを取得
        router.events
            // NavigationEndイベントだけにする
            .filter(e => e instanceof NavigationEnd)

            // NavigationEnd型に変換
            .map(e => e as NavigationEnd)

            // イベント購読
            .subscribe(e => {
                // トラッカーを更新
                ga('set', 'page', e.url);

                // PV送信
                ga('send', 'pageview', e.url);
            });
    }
}
```

# :five: 終わりに
記事自体のクオリティがかなり荒く申し訳ないですが何方かの参考になれば幸いです。
まずは何かアプトプットするということが大事かと思いましたのでご容赦を^^