Frontend(FirestoreをNuxt.js)にbackend(python)の進行状況に対応するprogress barを表示する
目的: backendの状況をfirestoreを通してfrontendに伝える
背景: backendに重い処理(deep learningなど)をfrontendから実行したときに、frontendでbackendの進捗を確認できないといつ終わるかわからずに辛い
前提
- frontendはnuxt.js (vuex)
- vuexfireを使う (firestoreとvuexのデータをreactiveにbindするパッケージ)
- BDはcloud firestore (firebaseのNoSQLDBサービス)
- backendはpython
version
- nuxt: 2.14.12
- vuexfire: 3.2.0-alpha.0
- firebase: 9.2.7
- firebase-tools: 9.3.0
- core-js: 3.8.3
- @nuxtjs/axios: 5.12.5
実装
backendのセットアップ
pip install --upgrade firebase-admin
firebaseの認証情報を手に入れる
この情報は firebase consoleでプロジェクトを作って、プロジェクトの概要の歯車からプロジェクトを設定、サービスアカウントタブ
Firebas Admin SDKを選び新しい秘密鍵の生成から作れる
データの書き込みの練習
import firebase_admin
from firebase_admin import credentials
from firebase_admin import firestore
cred = credentials.Certificate("./serviceSccountKey.json") #さっきダウンロードしたファイル
firebase_admin.initialize_app(cred) # 1度だけ呼ぶ
db = firestore.client()
db.collection('posts').document('aa').set(
{'message': 'にぱー'}
)
firestoreのデータ構造はcollection(ディレクトリみたいなもの)/doucment(ファイル名)/data(ファイル内のデータ(dict))という単位で管理されている
今回ははpostsというcollectionをつくり {'message': 'にぱー'}というデータのaaというファイルを書き込んだ. (結果はwebで確認できる(firebaseのコンソールからfirestoreでみれる))
ちなみファイル名に当たるdocumentは省略可能で普通は省略してランダムな文字列にする
db.collection('posts').document().set({'message': 'にぱー'})
db.collection('posts').document().set({'message': 'ああああああ'})
db.collection('posts').document().set({'message': 'かわいそかわいそなのです'})
ちなみにfirebaseのコンソールから確認するとこんな感じ
今回は必要ないがデータの取得もかんたん
import firebase_admin
from firebase_admin import credentials
from firebase_admin import firestore
cred = credentials.Certificate("./serviceSccountKey.json") #さっきダウンロードしたファイル
firebase_admin.initialize_app(cred) # 1度だけ呼ぶ
db = firestore.client()
docs = db.collection('posts').get()
for doc in docs:
print(doc.to_dict())
実用上は絞り込み大事
query = db.collection('posts').where('message', '==', 'かわいそかわいそなのです')
docs = query.get()
for doc in docs:
print(doc.to_dict())
progressbarのためのデータ書き込み
import time
import firebase_admin
from firebase_admin import credentials
from firebase_admin import firestore
cred = credentials.Certificate("./serviceSccountKey.json") #さっきダウンロードしたファイル
firebase_admin.initialize_app(cred) # 1度だけ呼ぶ
db = firestore.client()
for i in range(0, 101, 5):
db.collection('progress').document('1').set(
{'value': i}
)
time.sleep(0.001)
firesoter上で
progress/1/{'value': ここの数字を0~9に変化させる}
※本当はsetではなくupdateで値を変更したほうがいいかも(setはdocを新しく作成し、updateはdocそのままでdataを更新する)
frontend(nuxt.js + firebase)のセットアップ
yarn create nuxt-app vuexfire
cd vuexfire
vuexfireは今回のプロジェウト名で任意の名前で良い
以後path/to/vuexfireを~と書く、存在しないファイルは基本新規作成で
package install
yarn create nuxt-app vuexfire
yarn add nuxt
yarn add firebase
yarn add firebase-tools
firebaseと連携するための認証情報を追加
import firebase from 'firebase'
if (!firebase.apps.length) {
firebase.initializeApp({
apiKey: "xxx",
authDomain: "xxx",
projectId: "xxx",
storageBucket: "xxxx",
messagingSenderId: "xxxx",
appId: "xxxxx"
})
}
export default firebase
この情報は firebase consoleでプロジェクトを作って、プロジェクトの概要の歯車からプロジェクトを設定、全般タブの
ウェブアプリのFirebase SDK snippetの構成から取得できる
pluginを以下のように追加 (他の場所はそのまま)
plugins: [
'@/plugins/firebase'
]
nuxt.js + vuexfireの使い方の練習
<template>
<section id="main">
<!-- データの入力 -->
<textarea v-model="message" placeholder="Please enter a comment(Within 100 characters)" maxlength="100"></textarea>
<div class="submitBtn" v-on:click="sendData">
Submit
</div>
<ul>
<!-- リスト形式データの表示 -->
<li v-for="post in posts" v-bind:key="post.id">
{{post.message}}
</li>
<!-- progressの数字を表示 -->
<h1>Progress</h1>
{{progress.value}}
</ul>
</section>
</template>
<script>
import axios from 'axios';
import firebase from "@/plugins/firebase.js";
import { mapGetters} from 'vuex';
const db = firebase.firestore();
export default {
data() {
return {
message: "",
}
},
computed: {
// VuexからPostsデータを取得
...mapGetters(['posts']),
...mapGetters(['progress']),
},
created: function () {
// firestoreのpostsをバインド dbをバインドするための情報を渡してる
this.$store.dispatch('setPostsRef', db.collection('posts'));
this.$store.dispatch('setProgressRef', db.collection('progress').doc('1'));
},
methods: {
sendData: function () {
// データのチェック
if (this.message == "" || this.message.length > 100) {
return false;
}
let dbdata = {
message: this.message
};
// データの登録
db.collection('posts').add(dbdata);
}
}
}
</script>
createdでvuexとfirestoreのデータをbindする(これでつねにsyncしてる感じになる)
computedでfirestoreのデータを呼び出している
methodでfirestoreにmessageを追加する(pythonのときのようにdbを直接編集すればいい)
上で使ったthis.$store.dispatch, ...mapGettersの命令をvuex側で用意する
import { vuexfireMutations, firestoreAction } from 'vuexfire';
export const state = () => ({
posts: [],
progress: {'value': 0},
});
export const mutations = {
...vuexfireMutations
};
export const actions = {
setPostsRef: firestoreAction(function (context, ref) {
context.bindFirestoreRef('posts', ref)
}),
setProgressRef: firestoreAction(function (context, ref) {
context.bindFirestoreRef('progress', ref)
}),
};
export const getters = {
posts: state => state.posts,
progress: state => state.progress
};
上で使ったポイントは
setPostsRef: firestoreAction(function (context, ref) {
context.bindFirestoreRef('posts', ref)
})
の第2引数refは呼び出し側の引数, db.collection('posts')がはいりdocuments全体(ファイル名のリスト)が入力されてposts = []とbindされた状態になる. 例えばprogressの方はrefがdb.collection('progress').doc('1'))となり1つのdocument(ファイル1つ)が progress={'value': 0}にbindする.
frontend(nuxt.js + firebase)のprogress barを作る
vue-ellipse-progressのインストール
yarn add vue-ellipse-progress
installしたvueパッケージを使うためにはpluginsとnuxt.config.jsを編集
import Vue from 'vue'
import VueEllipseProgress from 'vue-ellipse-progress'
Vue.use(VueEllipseProgress)
pluginを以下のように追加 (他の場所はそのまま)
plugins: [
'@/plugins/firebase',
{
src: '@/plugins/progress',
mode: 'client'
}
progressbarのところはpages/index.vueに直接書き込むとごちゃごちゃするのでcomponentsに切り出す
<template>
<div class="container">
<vue-ellipse-progress
:data="circles"
:progress="progress"
:angle="-90"
:color="colorFillGradient"
emptyColor="#6546f7"
:emptyColorFill="emptyColorFillGradient"
:size="300"
:thickness="10"
emptyThickness="10%"
lineMode="in 10"
:legend="true"
:legendValue="progress"
legendClass="legend-custom-style"
dash="60 0.9"
animation="rs 0 0"
:noData="false"
:loading="loading"
fontColor="#000"
:half="false"
:gap="10"
dot="10 blue"
fontSize="5rem">
<span slot="legend-value">/100</span>
<p slot="legend-caption">GOOD JOB</p>
</vue-ellipse-progress>
</div>
</template>
<script>
export default {
props: ['progress'],
data() {
return {
colorFillGradient: {
radial: false,
colors: [
{
color: '#6546f7',
offset: 0,
opacity: '1',
},
{
color: 'lime',
offset: 100,
opacity: '0.6',
},
]
}
}
}
}
</script>
<style>
.container {
margin-top: 300px;
min-height: 100vh;
justify-content: center;
align-items: center;
text-align: center;
font-size: 15px;
}
</style>
componentsに切り出した自作タグProgressを読み込んで使う
<template>
<section id="main">
<!-- データの入力 -->
<textarea v-model="message" placeholder="Please enter a comment(Within 100 characters)" maxlength="100"></textarea>
<div class="submitBtn" v-on:click="sendData">
Submit
</div>
<ul>
<!-- リスト形式データの表示 -->
<li v-for="post in posts" v-bind:key="post.id">
{{post.message}}
</li>
<!-- progressの数字を表示 -->
</ul>
<h1>Progress</h1>
{{progress.value}}
<Progress :progress="progress.value"/>
</section>
</template>
<script>
import axios from 'axios';
import firebase from "@/plugins/firebase.js";
import { mapGetters} from 'vuex';
import Progress from "@/components/Progress.vue"
const db = firebase.firestore();
export default {
data() {
return {
message: "",
increasing_pct: 0,
}
},
computed: {
// VuexからPostsデータを取得
...mapGetters(['posts']),
...mapGetters(['progress']),
},
created: function () {
// firestoreのpostsをバインド dbをバインドするための情報を渡してる
this.$store.dispatch('setPostsRef', db.collection('posts'));
this.$store.dispatch('setProgressRef', db.collection('progress').doc('1'));
},
methods: {
sendData: function () {
// データのチェック
if (this.message == "" || this.message.length > 100) {
return false;
}
let dbdata = {
message: this.message
};
// データの登録
db.collection('posts').add(dbdata);
}
}
}
</script>
最終的に出来上がったもの
参考文献
Discussion