AnglarのビルドとFirebaseのデプロイを連動させる

> この記事は[Angular+Firebaseでチャットアプリを作る](http://qiita.com/Yamamoto0525/items/a76ea4b3924eeb82b0f9)のエントリーです。
> 前記事：[WEBアプリでFirebaseのデプロイ環境を構築する](https://qiita.com/Yamamoto0525/items/782a84cc92a14b35786c)

# この記事で行うこと

[前回の記事](https://qiita.com/Yamamoto0525/items/782a84cc92a14b35786c)ではFirebaseのステージング運用とデプロイ方法について書きましたが、今回の記事ではAngularとFirebaseのデプロイを連動させる方法について書いていきます。

- [開発環境にデプロイする](#開発環境にデプロイする)
- [ステージング環境にデプロイする](#ステージング環境にデプロイする)
- [本番環境にデプロイする](#本番環境にデプロイする)

### Angularのデプロイ

Angularに限らず、近年のJavaScriptアプリケーションでは開発環境と本番環境のビルドファイルを分けて作成することが多いです。開発環境ではデバッグをする必要があるため、SourceMapの表示を許容し、デバッグのしやすさを重視します。一方、本番環境ではファイルの読み込み速度や機密性が重視されるため、ファイルをminifyし、余計な情報を減らす作業が必要になります。

このビルド作業の使い分けは自力でやろうとすると面倒が多いため、webpackやgulpといったタスクランナーを使って管理することがデファクトスタンダードになっています。
Angularでも例に漏れず、裏側ではwebpackを使ってタスク処理をしているようですが、`webpack.config.js`ではなく、`angular.json`にビルド方式やテストツールの設定情報を記載しています。
「自前のwebpackでやりたい！」という方向けの`@angular-builders/custom-webpack`というものもあるので、自分で設定を書きたい方は以下のリンクを参考にしてください。

参考：[Angular + カスタマイズWebpack 開発環境構築](https://qiita.com/niyou0ct/items/290d74d8be86eae4e358)

# 実装内容

AngularとFirebaseを連動させてデプロイを行う場合は、AngularFireの`ng deploy`を使用します。このコマンドにより、開発環境に応じたビルド、デプロイを実現できるようになります。
AngularFireについては既に[以前の記事](https://qiita.com/Yamamoto0525/items/437a2884c0c51f5a3af8)でインストール方法等について紹介していますので、この記事から参照された方はそちらを確認してください。

参考：https://angular.io/guide/deployment

> 注1）`ng deploy`が`@angular/cli`に導入されたのはv8.3.0以降となります。それ以前の`@angular/cli`を利用している場合は使用できませんのでご注意ください。
> 注2）`@angular/cli`の`ng deploy`は2020年の5月にバグが報告され、修正されています。下記の実装はそれ以前の`@angular/cli`だとエラーがでますので、最新の状態にしてください。
> 参考：https://github.com/angular/angular-cli/issues/17613

なお、今回使用するFirebaseのプロジェクトは２つで、次の様に設定します。

| 環境  | Project | ビルド方式 |
|:-----------------|:------------------|:------------------|
|開発環境|開発用|開発用|
|ステージング環境 |開発用|本番用|
|本番環境|本番用|本番用|

## 開発環境にデプロイする

まず、`angular.json`のソースコードを確認します。

```json:angular.json
"deploy": {
 "builder": "@angular/fire:deploy",
 "options": {},
}
```
`@angular/cli`v8.3.0以降の`angular.json`であれば、最初から`ng deploy`のための設定が記載されています。とりあえずこの状態で`ng deploy`を実行すると、`ng build --prod`コマンドを打った時と同じ挙動をします。

ただ、開発環境ではデバッグをする必要があるため、`--prod`をしないように設定する必要があります。

```json:angular.json
"deploy": {
 "builder": "@angular/fire:deploy",
 "options": {
     "buildTarget": "NgChat:build",
 },
}
```

これで開発環境の設定は完了です。
`ng deploy`をして、開発環境の挙動を確認します。

### 実行結果

デプロイ後の画面（ソースマップが確認できる）
<img width="1317" alt="スクリーンショット 2020-08-08 23.17.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112566/6600915f-faeb-087d-5e1a-1e397067cbc9.png">


## ステージング環境にデプロイする

まず、staging環境用の環境設定ファイルを作成します。
開発環境の`src/environments/environment.ts`をコピーし、`production`をtrueにします。

```ts:src/environments/environment.staging.ts
export const environment = {
  production: true,
  firebase: {
    apiKey: '<your-key>',
    authDomain: '<your-project-authdomain>',
    databaseURL: '<your-database-URL>',
    projectId: '<your-project-id>',
    storageBucket: '<your-storage-bucket>',
    messagingSenderId: '<your-messaging-sender-id>',
    appId: '<your-app-id>',
    measurementId: '<your-measurement-id>',
  },
};
```
次に`angular.json`でビルド情報を設定します。buildオブジェクトにあるconfigurationsを見ると、すでにproduction用の設定は記載されているので、この内容をstaging用に修正します。
＊設定ファイルパス以外は変更していません。

```json:angular.json
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.staging.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb",
                  "maximumError": "10kb"
                }
              ]
            }
          }
```
ここに書いてある設定情報が何を意味しているのかをざっくりまとめると、以下のようになります。

| 項目名  | 説明 |
|:-----------------|:------------------|
| fileReplacements|ファイル差し替え。主にenvironmentファイルに使われる。| 
| optimization |最適化する。{scripts: true, styles: false} というような個別設定も可能|
| outputHashing|出力ファイル名にハッシュをつける。キャッシュバスター。"none","all","media","bundles"|
| sourceMap|sourceMapを出力する。{scripts: true, styles: true, hidden: false, vendor: true}というような個別設定も可能|
| extractCss|グローバル指定のcssを展開する|
| namedChunks|遅延読み込みのファイルに名前をつける|
| extractLicenses|利用ライブラリのライセンスファイルをまとめる|
| vendorChunk|ライブラリだけで単独のファイルにする。ライブラリは変更頻度が低いため。|
| buildOptimizer|aot利用時、@angular-devkit/build-optimizerを有効にする|
| budgets|生成ファイルのファイルサイズ制限を設定できる。気軽に巨大なライブラリをimportすると使わないコードが大量に含まれてしまったりするのを警告する。|

> この内容は以下記事からの抜粋です。他のパラメータについてはそちらを参照してみてください。
> 参考：[angular.jsonの中身](https://qiita.com/m4fg/items/6426375ae64762c93124)

ここで指定した内容をもとにデプロイできるよう、`angular.json`のデプロイ情報に追記します。

```json:angular.json
        "deploy": {
          "builder": "@angular/fire:deploy",
          "options": {
            "buildTarget": "NgChat:build"
          },
          "configurations": {
            "staging": {
              "buildTarget": "NgChat:build:production"
            }
          }
        }
```
これで準備が整いました。
あとは`-c`オプション(`--configuration`のエイリアス)をつけてデプロイします。

```bash
ng deploy -c staging
```

### 実行結果

デプロイ後の画面（ソースマップが確認できず、エラーは表示される）
<img width="1422" alt="スクリーンショット 2020-08-08 23.50.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112566/5d6bdeee-75e2-9307-2488-ba2327a04e4d.png">


## 本番環境にデプロイする

最後に本番環境のデプロイ設定します。
Angularのプロジェクトにある`environment.prod.ts`ファイルにfirebaseの情報を追記します。この際、本番用プロジェクトのパラメータを使用してください。

```ts:src/environments/environment.prod.ts
export const environment = {
  production: true,
  firebase: {
    apiKey: '<your-key>',
    authDomain: '<your-project-authdomain>',
    databaseURL: '<your-database-URL>',
    projectId: '<your-project-id>',
    storageBucket: '<your-storage-bucket>',
    messagingSenderId: '<your-messaging-sender-id>',
    appId: '<your-app-id>',
    measurementId: '<your-measurement-id>',
  },
};
```

次に`angular.json`に本番用プロジェクトの追加をします。
`serve`や`test`は開発・ステージング環境でのみ使用するため、本番用のプロジェクトからは除外しています。

```json:angular.json
    "NgChatProd": {
      "projectType": "application",
      "schematics": {},
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/NgChat",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "aot": true,
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "node_modules/bootstrap/dist/css/bootstrap.min.css",
              "src/styles.css"
            ],
            "scripts": []
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb",
                  "maximumError": "10kb"
                }
              ]
            }
          }
        },
        "deploy": {
          "builder": "@angular/fire:deploy",
          "options": {}
        }
      }
    }
```
この設定だけだどhostingデプロイ時にエラーがでるため、Firebase側の設定ファイルにも追記をします。
今回、ステージング用のFirebaseプロジェクトは使用しないため、開発用と本番用の２プロジェクトを作成します。

```json:.firebaserc
{
  "projects": {
    "dev": "開発用FirebaseプロジェクトID",
    "prod": "本番用FirebaseプロジェクトID"
  },
  "targets": {
    "開発用FirebaseプロジェクトID": {
      "hosting": {
        "NgChat": [
          "開発用FirebaseプロジェクトID"
        ]
      }
    },
    "本番用FirebaseプロジェクトID": {
      "hosting": {
        "NgChatProd": [
          "本番用FirebaseプロジェクトID"
        ]
      }
    }
  }
}
```
```json:firebase.json
    {
      "target": "NgChat",
      "public": "dist/NgChat",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    },
    {
      "target": "NgChatProd",
      "public": "dist/NgChat",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    }
```
これで本番環境の設定は完了です。`ng deploy`の後にプロジェクト名をいれて実行します。

```bash
ng deploy NgChatProd
```

### 実行結果

デプロイ後の画面（ソースマップが確認できず、エラーは表示される）
<img width="1415" alt="スクリーンショット 2020-08-08 23.54.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112566/0d986279-71e3-a575-8c57-524d0ba08bd6.png">


本記事をもって「Angular+Firebaseでチャットアプリを作る」のエントリーはすべて終了です。
お疲れ様でした。

### ソースコード

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