Firestoreのデータを可視化・監視しアラートする、かんたんなWEBアプリ(Nuxt.js,GCP Firestore)

# はじめに
Nuxt.jsの勉強がてら、Firestoreに格納されているデータをブラウザに表示させつつ、リアルタイムに変更があった際に、その更新が反映される簡易WEBアプリを作成しました。

アクションとしては、３つの仕組みを用意しました。
①トグルボタンでのFirestoreデータ更新(True/False)
②削除ボタンでのFirestoreデータ削除
③更新（追加、変更、削除）があった際、ブラウザ上アラートが上がる

作成したNuxt.jsプロジェクトは、Firebase Hostingにデプロイして動作確認し、
手動デプロイができたので、GCP CloudBuildを使って、CIの仕組みも少し試験しました。

見た目はこんなものです。（CSSについては特に今回は言及しません。）
このカラム名で、データがドキュメント毎に保存（図にあるのは二つ）されている形になります。

![UI](https://storage.googleapis.com/permanent_koizumi/20191129_ui.png "UI")



##技術要素

* Firestore
* vue.js / Nuxt.js
* Firebase Hosting
* GCP CloudBuild

# 説明


+ 1. create-nuxt-appでNuxt.jsプロジェクト作成
+ 2. Firestoreへ接続するコード準備
+ 3. Storeの設定
+ 4. 表示部分のページ作成
+ 5. ローカルで動作確認
+ 6. Firebase Hostingへデプロイ
+ 7. CloudBuildでCI環境構築

## 1. create-nuxt-appでNuxt.jsプロジェクト作成 
まず、SEOなどは気にせず、今回はSPA(Single Page Application)でお手軽に作成したいので、SPAを選択しました。
参考サイトは、[Nuxt.jsを使うときに、SPA・SSR・静的化のどれがいいか迷ったら](https://qiita.com/nishinoshake/items/f42e2f03663b00b5886d
 "Nuxt.jsを使うときに、SPA・SSR・静的化のどれがいいか迷ったら")です。

基本的にはデフォルトで、SPAを選択する箇所のみ変更。

```Javascript:
npx create-nuxt-app nuxt-view-firestore-status
```

必要に応じて、npmパッケージはインストールしておきます。後々利用します。

```Javascript:
npm install --save firebase
npm install --save vuexfire
```



## 2. Firestoreへ接続するコード準備

必須ではないですが、dotenvを使って環境変数にプロジェクトIDと後々使うFirestoreのコレクション名を格納しようと思います。nuxt.config.jsのmodulesに、にdotenvを設定。
そして、.envファイルは、作成されたフォルダ直下に新規作成。

```Javascript:nuxt.config.js
  modules: [
    '@nuxtjs/dotenv'
  ],
```

```Javascript:.env
FIREBASE_PROJECT_ID = '<your project id>'
FIRESTORE_COLLECTION_NAME = '<your firestore collection name>'
```

次に、Firestoreの初期コンフィグを定義する、Vueプラグインを作成します。pluginsフォルダ配下に、firebase.jsを作成します。

```Javascript:plugins/firebase.js
import firebase from 'firebase'

const config = {
    projectId: process.env.FIREBASE_PROJECT_ID
}

if (!firebase.apps.length){
    firebase.initializeApp(config)
}

export default firebase
```



## 3. Storeの設定
今回は、ストアの利用方法の「クラシックモード」と「モジュールモード」のうち、モジュールモードで作成します。構成は以下です。

[Nuxt.jsのストアをモジュールモードで使用するときのTips](https://qiita.com/y-miine/items/028c73aa3f87e983ed4c "Nuxt.jsのストアをモジュールモードで使用するときのTips")

[Nuxt.js + Firebase で vuexfire を使って Cloud Firestore と連携する(データバインド編)](https://volpe.hatenablog.com/entry/2019/04/01/094815
 "Nuxt.js + Firebase で vuexfire を使って Cloud Firestore と連携する(データバインド編)")


```
store
├── index.js
└── view_status.js
```

index.jsでは、vuexfire の vuexfireMutations を定義するのみ。

```Javascript:store/index.js
import { vuexfireMutations } from 'vuexfire'

export const mutations ={
    ...vuexfireMutations
}
```

次に、view_status.jsに、Firestoreの読み込みと、ステート、アクション定義。

|アクション名|説明|
|:---|:---|
|init|Firestoreから取得するデータをバインド|
|remove|削除ボタンが押されたときに呼び出す。ドキュメント名を引数にFirestoreのドキュメント削除|
|toggle|トグルボタンが押されたときに呼び出す。対象ドキュメントのkeyがdoneの値をTrue/Falseへ変更させる。(Todosサンプルアプリの流用なのであまり気にしないでください。)|
|popup|Firestoreの対象Collection内のドキュメントが追加、削除、更新が発生した際に、アラートを上げる機能|

```Javascript:store/view_status.js
import firebase from '~/plugins/firebase'
import { firestoreAction } from 'vuexfire'

const db = firebase.firestore()
const statusRef = db.collection(process.env.FIRESTORE_COLLECTION_NAME)

export const state = () => ({
  statuses: []
})

export const actions = {
  init: firestoreAction(({ bindFirestoreRef }) => {
    bindFirestoreRef('statuses', statusRef)
  }),
  remove: firestoreAction((context, id) => {
    statusRef.doc(id).delete()
  }),
  toggle: firestoreAction((context, status) => {
    statusRef.doc(status.id).update({
      done: !status.done
    })
  }),
  popup: firestoreAction((context) => {
    statusRef.onSnapshot(function(snapshot){
      snapshot.docChanges().forEach(function(change){
        if (change.type === "added"){
          alert("ドキュメントが追加されました。\r\nIDは、" + change.doc.id + "です。")
        }
        if (change.type === "modified"){
          alert("ドキュメントが変更されました。\r\nIDは、" + change.doc.id + "です。")
        }
        if (change.type === "removed"){
          alert("ドキュメントが削除されました。\r\nIDは、" + change.doc.id + "です。")
        }
      })
    })
  })
}
```

### 注意

FirestoreのCollectionのルール設定は、今回、開けてテストを実施しましたが、本番利用時はセキュリティ設計の考慮が必要です。FirebaseコンソールのDatabase -> 「ルール」でGUIで設定可能です。

Firebaseのサイトの[安全でないルールを修正する](https://firebase.google.com/docs/firestore/security/insecure-rules?hl=ja#common_scenarios_with_insecure_rules "安全でないルールを修正する")を参照。


```Javascript:
    match /<your collection name>/{document=**} {
      allow read, write: if true;
    }
```

## 4. 表示部分のページ作成
最後に、表示部分のページ作成するため、pagesフォルダに、閲覧用のVueファイルを作成します。status.vueとしました。

| script　　　　　 | 説明 |
|:---|:---|
|created|ページが開いた時の処理。Vue インスタンスが生成された直後に呼び出されます。今回は、上記で定義した、initとpopupアクションを呼び出しています。|
|methods|vueテンプレートから呼び出すメソッドで、remove(id)とtoggle(status)を定義しました。上記で定義したremoveとtoggleアクションを引数設定しつつ呼び出します。|
|computed|値が変わると再評価される仕組みで、今回はstoreの値を見て変化があれば更新されるようにしています。|

```Javascript:pages/status.vue（抜粋）
    created: function() {
      this.$store.dispatch('view_status/init')
      this.$store.dispatch('view_status/popup')
    },
    methods: {
      remove(id) {
        this.$store.dispatch('view_status/remove', id)
      },
      toggle(status) {
        this.$store.dispatch('view_status/toggle', status)
      },
    },
    computed: {
      view_status() {
        return this.$store.getters['view_status/orderedStatus']
      }
    },
```

テーブルで取得したFirestoreのデータを表示しています。

```html:pages/status.vue（抜粋）
<template>
  <div>
    <table>
      <tr>
        <th>-</th>
        <th>done</th>
        <th>session_id</th>
        <th>url</th>
        <th>timestamp</th>
        <th>remove</th>
      </tr>
      <tr v-for="status in view_status" :key="status.id">
        <td>
          <input type="checkbox" v-bind:checked="status.done" @change="toggle(status)">
        </td>

        <td>{{status.done}}</td>
        <td> {{status.session_id}}</td>
        <td> <a v-bind:href="status.url">{{status.url}}</a></td>
        <td> {{status.timestamp.toDate() | dateFilter}}</td>
        <td>
          <button v-on:click="remove(status.id)">remove</button>
        </td>

      </tr>
    </table>
  </div>
</template>
```

簡単にコメントすると、

```html:pages/status.vue（抜粋）
<tr v-for="status in view_status" :key="status.id">
```

view_statusで取得した値（＝ドキュメント一覧）をドキュメント毎に、v-forで繰り返し処理しています。


```html:pages/status.vue（抜粋）
<input type="checkbox" v-bind:checked="status.done" @change="toggle(status)">
```

UI上、一番左のカラムにチェックボックスを配置し、チェックON/OFFは、ドキュメント（status）のdoneの値をバインドしています。変更があった場合は、toggleメソッドを呼び出します。

```html:pages/status.vue（抜粋）
<a v-bind:href="status.url">{{status.url}}</a>
```

ドキュメント（status）のurlの値にURLの文字列が格納されているのですが、最初はmustacheタグ{{status.url}}で表示させようとしましたが、それだとリンクがつかないので、aタグでリンクさせたい場合は、このようにバインドさせて表示させる必要があるようです。

```html:pages/status.vue（template内）
<td> {{status.timestamp.toDate() | dateFilter}}</td>
```

```Javascript:pages/status.vue（script内）
  import moment from 'moment'
//（省略）
    filters: {
      dateFilter: function(date) {
        return moment(date).format('YYYY/MM/DD HH:mm:ss')
      }
    }
```

タイムスタンプの値を、指定の形式で表示させようとする場合は、フィルタ機能を使って実現させると良いようです。momentライブラリを使いました。

```html:pages/status.vue（抜粋）
  <td>
     <button v-on:click="remove(status.id)">remove</button>
  </td>
```



### timestampを降順にする
computedのところでは、storeのgettersを呼び出していますが、timestampの順序を変える仕組みを導入しました。日付でソートをかけるために、webpackプラグイン(lodash)をinstallしています。

[webpack プラグインを追加するには？](https://ja.nuxtjs.org/faq/webpack-plugins/ "webpack プラグインを追加するには？")と、[細かすぎるけど伝わって欲しいlodash.jsの話](https://techblog.kayac.com/2017-12-2_lodash "細かすぎるけど伝わって欲しいlodash.jsの話")を参照。


```Javascript:nuxt.config.js
const webpack = require('webpack')

// (省略)

  build: {
    /*
    ** You can extend webpack config here
    */
    extend (config, ctx) {
    },
    plugins: [
      new webpack.ProvidePlugin({
        '_': 'lodash'
      })
    ]
  }
```

そのうえで、gettersをview_status定義し、降順でtimestampが並ぶように処理をします。

```Javascript:store/view_status.js
export const getters = {
  orderedStatus: state => {
    return _.orderBy(state.statuses, 'timestamp', 'desc')
  }
}
```


## 5. ローカルで動作確認
ローカルでの検証は ``` npm run dev ```で動作させ、localhost:3000/status上で正常に動くことを確認しました。

再掲になりますが、以下のようなUIで、動作が問題なく動きました。
+ 一番左のチェックボックスをクリックすると、Doneの値がtrueへ更新
+ 再度クリックするとDoneの値がfalseに更新
+ 一番右のremoveボタンをクリックすると対象データが削除
+ 上記アクションを実施した際に、アラートが画面上上がること

![UI](https://storage.googleapis.com/permanent_koizumi/20191129_ui.png "UI")

## 6. Firebase Hostingへのデプロイ
まず、Nuxtプロジェクトをビルドします。``` npm run build```
Firebaseプロジェクトを ``` firebase init ``` コマンドで初期化します。

注意としては、hostingを選択することと、build後に生成されるフォルダはdist(not public)なので、distを指定することです。

上記を実施後、``` firebase deploy ```コマンドでデプロイします。
問題がなければ、``` https://<your project>.firebaseapp.com/status/``` などにアクセスして確認できます。


## 7. CloudBuildでCI環境構築
CloudBuildでCI環境構築構築し、githubにコードをデプロイすると、CloudBuildがトリガーされ、FirebaseHostingへ自動デプロイの仕組みも作りました。CloudBuild側の設定は、別途サイトを参考ください。
準備としては、Dockerfileを作成し、cloudbuild.yamlで設定を入れることで動作しました。
ちなみに、```.gitignore```ファイルに```.env```が記載されているので、CloudBuildで連携する場合は、.envもリポジトリに上げるようにする必要があります。

```Docker:Dockerfile
# use latest Node 10
FROM node:10
# install Firebase CLI
RUN npm install -g firebase-tools

ENTRYPOINT ["/usr/local/bin/firebase"]
```

```yaml:
steps:
# Install
- name: 'gcr.io/cloud-builders/npm'
  args: ['install']
# Build
- name: 'gcr.io/cloud-builders/npm'
  args: ['run', 'build']
# Docker Image
- name: 'gcr.io/cloud-builders/docker'
  args: ['build', '-t', 'gcr.io/$PROJECT_ID/firebase', '.']
# Deploy
- name: 'gcr.io/$PROJECT_ID/firebase'
  args: ['deploy', '--project', '$PROJECT_ID']
# push built images to Container Registry
images: ['gcr.io/$PROJECT_ID/firebase']
```



#おわりに
Vue.js / Nuxt.js のおかげで比較的簡単に、ブラウザ上でFirestoreデータを確認し、更新があったらアラートが上がる機能を作成できました。

もう少し応用的なものも作ってみたいと思うのと、環境面では、FirebaseHostingではなく、Google App Engineにもデプロイができるようなので、そちらも試してみたいと思います。

[コードはgithubに公開しました。](https://github.com/ko-ism/nuxt-view-firestore-status)

## 参考コンテンツ
* Udemy 中村祐太「Nuxt JS入門決定版！Vue.jsのフレームワークNuxt JSの基本からFirebaseと連携したSPAの開発まで」
* [Nuxt.js プロジェクトを Firebase Hosting にデプロイして公開する方法](https://upd.world/nuxtjs-deploy-firebase-hosting/
 "Nuxt.js プロジェクトを Firebase Hosting にデプロイして公開する方法")
* [Nuxt.jsを使うときに、SPA・SSR・静的化のどれがいいか迷ったら](https://qiita.com/nishinoshake/items/f42e2f03663b00b5886d
 "Nuxt.jsを使うときに、SPA・SSR・静的化のどれがいいか迷ったら")
* [Nuxt.jsのストアをモジュールモードで使用するときのTips](https://qiita.com/y-miine/items/028c73aa3f87e983ed4c "Nuxt.jsのストアをモジュールモードで使用するときのTips")
* [Nuxt.js + Firebase で vuexfire を使って Cloud Firestore と連携する(データバインド編)](https://volpe.hatenablog.com/entry/2019/04/01/094815
 "Nuxt.js + Firebase で vuexfire を使って Cloud Firestore と連携する(データバインド編)")
* [細かすぎるけど伝わって欲しいlodash.jsの話](https://techblog.kayac.com/2017-12-2_lodash "細かすぎるけど伝わって欲しいlodash.jsの話")