Frontend(FirestoreをNuxt.js)にbackend(python)の進行状況に対応するprogress barを表示する

11 min read読了の目安(約10500字

目的: 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のためのデータ書き込み

main.py
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と連携するための認証情報を追加

~/plugins/firebase.js
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を以下のように追加 (他の場所はそのまま)

~/nuxt.config.js
  plugins: [
    '@/plugins/firebase'
    ]

nuxt.js + vuexfireの使い方の練習

~/pages/index.vue
<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側で用意する

~/store/index.js
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を編集

~/plugins/progress.js
import Vue from 'vue'
import VueEllipseProgress from 'vue-ellipse-progress'
Vue.use(VueEllipseProgress)

pluginを以下のように追加 (他の場所はそのまま)

~/nuxt.config.js
  plugins: [
    '@/plugins/firebase',
    {
      src: '@/plugins/progress',
      mode: 'client'
    }

progressbarのところはpages/index.vueに直接書き込むとごちゃごちゃするのでcomponentsに切り出す

~/components/Progress.vue
<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を読み込んで使う

~/pages/index.vue
<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>

最終的に出来上がったもの

参考文献