はじめに
Nuxt.jsの勉強がてら、Firestoreに格納されているデータをブラウザに表示させつつ、リアルタイムに変更があった際に、その更新が反映される簡易WEBアプリを作成しました。
アクションとしては、3つの仕組みを用意しました。
①トグルボタンでのFirestoreデータ更新(True/False)
②削除ボタンでのFirestoreデータ削除
③更新(追加、変更、削除)があった際、ブラウザ上アラートが上がる
作成したNuxt.jsプロジェクトは、Firebase Hostingにデプロイして動作確認し、
手動デプロイができたので、GCP CloudBuildを使って、CIの仕組みも少し試験しました。
見た目はこんなものです。(CSSについては特に今回は言及しません。)
このカラム名で、データがドキュメント毎に保存(図にあるのは二つ)されている形になります。
##技術要素
- Firestore
- vue.js / Nuxt.js
- Firebase Hosting
- GCP CloudBuild
説明
-
- create-nuxt-appでNuxt.jsプロジェクト作成
-
- Firestoreへ接続するコード準備
-
- Storeの設定
-
- 表示部分のページ作成
-
- ローカルで動作確認
-
- Firebase Hostingへデプロイ
-
- CloudBuildでCI環境構築
1. create-nuxt-appでNuxt.jsプロジェクト作成
まず、SEOなどは気にせず、今回はSPA(Single Page Application)でお手軽に作成したいので、SPAを選択しました。
参考サイトは、Nuxt.jsを使うときに、SPA・SSR・静的化のどれがいいか迷ったらです。
基本的にはデフォルトで、SPAを選択する箇所のみ変更。
npx create-nuxt-app nuxt-view-firestore-status
必要に応じて、npmパッケージはインストールしておきます。後々利用します。
npm install --save firebase
npm install --save vuexfire
2. Firestoreへ接続するコード準備
必須ではないですが、dotenvを使って環境変数にプロジェクトIDと後々使うFirestoreのコレクション名を格納しようと思います。nuxt.config.jsのmodulesに、にdotenvを設定。
そして、.envファイルは、作成されたフォルダ直下に新規作成。
modules: [
'@nuxtjs/dotenv'
],
FIREBASE_PROJECT_ID = '<your project id>'
FIRESTORE_COLLECTION_NAME = '<your firestore collection name>'
次に、Firestoreの初期コンフィグを定義する、Vueプラグインを作成します。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
Nuxt.js + Firebase で vuexfire を使って Cloud Firestore と連携する(データバインド編)
store
├── index.js
└── view_status.js
index.jsでは、vuexfire の vuexfireMutations を定義するのみ。
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内のドキュメントが追加、削除、更新が発生した際に、アラートを上げる機能 |
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のサイトの安全でないルールを修正するを参照。
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の値を見て変化があれば更新されるようにしています。 |
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のデータを表示しています。
<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>
簡単にコメントすると、
<tr v-for="status in view_status" :key="status.id">
view_statusで取得した値(=ドキュメント一覧)をドキュメント毎に、v-forで繰り返し処理しています。
<input type="checkbox" v-bind:checked="status.done" @change="toggle(status)">
UI上、一番左のカラムにチェックボックスを配置し、チェックON/OFFは、ドキュメント(status)のdoneの値をバインドしています。変更があった場合は、toggleメソッドを呼び出します。
<a v-bind:href="status.url">{{status.url}}</a>
ドキュメント(status)のurlの値にURLの文字列が格納されているのですが、最初はmustacheタグ{{status.url}}で表示させようとしましたが、それだとリンクがつかないので、aタグでリンクさせたい場合は、このようにバインドさせて表示させる必要があるようです。
<td> {{status.timestamp.toDate() | dateFilter}}</td>
import moment from 'moment'
//(省略)
filters: {
dateFilter: function(date) {
return moment(date).format('YYYY/MM/DD HH:mm:ss')
}
}
タイムスタンプの値を、指定の形式で表示させようとする場合は、フィルタ機能を使って実現させると良いようです。momentライブラリを使いました。
<td>
<button v-on:click="remove(status.id)">remove</button>
</td>
timestampを降順にする
computedのところでは、storeのgettersを呼び出していますが、timestampの順序を変える仕組みを導入しました。日付でソートをかけるために、webpackプラグイン(lodash)をinstallしています。
webpack プラグインを追加するには?と、細かすぎるけど伝わって欲しいlodash.jsの話を参照。
const webpack = require('webpack')
// (省略)
build: {
/*
** You can extend webpack config here
*/
extend (config, ctx) {
},
plugins: [
new webpack.ProvidePlugin({
'_': 'lodash'
})
]
}
そのうえで、gettersをview_status定義し、降順でtimestampが並ぶように処理をします。
export const getters = {
orderedStatus: state => {
return _.orderBy(state.statuses, 'timestamp', 'desc')
}
}
5. ローカルで動作確認
ローカルでの検証は npm run dev
で動作させ、localhost:3000/status上で正常に動くことを確認しました。
再掲になりますが、以下のようなUIで、動作が問題なく動きました。
- 一番左のチェックボックスをクリックすると、Doneの値がtrueへ更新
- 再度クリックするとDoneの値がfalseに更新
- 一番右のremoveボタンをクリックすると対象データが削除
- 上記アクションを実施した際に、アラートが画面上上がること
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もリポジトリに上げるようにする必要があります。
# use latest Node 10
FROM node:10
# install Firebase CLI
RUN npm install -g firebase-tools
ENTRYPOINT ["/usr/local/bin/firebase"]
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にもデプロイができるようなので、そちらも試してみたいと思います。
参考コンテンツ
- Udemy 中村祐太「Nuxt JS入門決定版!Vue.jsのフレームワークNuxt JSの基本からFirebaseと連携したSPAの開発まで」
- Nuxt.js プロジェクトを Firebase Hosting にデプロイして公開する方法
- Nuxt.jsを使うときに、SPA・SSR・静的化のどれがいいか迷ったら
- Nuxt.jsのストアをモジュールモードで使用するときのTips
- Nuxt.js + Firebase で vuexfire を使って Cloud Firestore と連携する(データバインド編)
- 細かすぎるけど伝わって欲しいlodash.jsの話