Nuxt.jsプロジェクトでFirestoreを利用したデータの永続化/取り出しの実装方法

はじめに

数回に分けてNuxt.jsプロジェクトでデータの永続化を行う方法を解説しています。これらの記事は上から順番に読んでいくことを想定しています。

これらの記事で作成したNuxt.jsプロジェクトは以下のGitHubリポジトリーで公開しています。

GitHub - whitia/nuxt-firebase-sample

前提

Nuxt.jsプロジェクトを作成していない場合は、以下の記事を参考にして作成してください。

Nuxt.jsプロジェクトの作成から動作確認までの手順 - AUTOVICE

Firebaseの登録、プロジェクト作成、アプリ作成については、以下のページを参照してください。

Firebase を JavaScript プロジェクトに追加する

「ステップ 3: Firebase SDK を追加して Firebase を初期化する」の「Firebase 構成オブジェクト」は、plugins/firebase.jsを以下の内容で作成してください。

import firebase from 'firebase'

if (!firebase.apps.length) {
  firebase.initializeApp(
    {
      apiKey: "api-key",
      authDomain: "project-id.firebaseapp.com",
      databaseURL: "https://project-id.firebaseio.com",
      projectId: "project-id",
      storageBucket: "project-id.appspot.com",
      messagingSenderId: "sender-id",
      appID: "app-id",
    }
  )
}

export default firebase

ここで作成したプラグインはVuex(store/index.js)でインポートして使います。

import firebase from '~/plugins/firebase'

概念図

データの永続化

データの永続化と難しく言っていますが、要するにデータベースのような領域に保存するということです。

(1) ブラウザからVuexに定義したアクションを呼び出します。このとき永続化したいデータを引数で渡します。

(2) 受け取ったデータをFirebaseのFirestoreに保存します。

データの取り出し

(1) Vuexに定義したアクションからFirestoreにデータを取りに行きます。

(2) Firestoreからデータを受け取ります。

(3) 受け取ったデータをVuexに保存します。

(4) ブラウザからVuexに保存されたデータを取りに行きます。

実装

今回はブラウザからユーザーを作成する処理を実装します。あらかじめFirestoreにusresというコレクションを作成しておいてください。

データの永続化

処理の流れに沿って順番に実装していきます。UIフレームワークにBootstrap-Vueを使用していますので、適宜読み替えてください。Bootstrap-Vueについては以下を参照してください。

Nuxt.jsプロジェクトにBootstrap-Vueを追加する手順 - AUTOVICE

pages/users/create.vueを新規作成し、以下のように実装します。

<template>
  <b-form @submit.prevent="addUser">
    <div class="container mt-5">

      <div class="row justify-content-center mb-3">
        <div class="col-12 col-sm-3">
          <h2>Create user</h2>
        </div>
      </div>

      <div class="row justify-content-center">
        <div class="col-12 col-sm-3">
          <b-form-group label="First name" label-for="first">
            <b-form-input
              id="first"
              v-model="user.name.first"
              required
              placeholder="Jane"
            ></b-form-input>
          </b-form-group>
        </div>
      </div>

      <div class="row justify-content-center">
        <div class="col-12 col-sm-3">
          <b-form-group label="Last name" label-for="last">
            <b-form-input
              id="last"
              v-model="user.name.last"
              required
              placeholder="Doe"
            ></b-form-input>
          </b-form-group>
        </div>
      </div>

      <div class="row justify-content-center">
        <div class="col-12 col-sm-3">
          <b-form-group label="Age" label-for="age">
            <b-form-input
              id="age"
              v-model="user.age"
              required
              placeholder="20"
            ></b-form-input>
          </b-form-group>
        </div>
      </div>

      <div class="row justify-content-center mt-3">
        <div class="col-12 col-sm-3">
          <b-button block type="submit" variant="primary">Create</b-button>
        </div>
      </div>

    </div>
  </b-form>
</template>

<script>
export default {
  data() {
    return {
      user: {
        name: {
          first: null,
          last: null
        },
        age: null
      }
    }
  },
  methods: {
    addUser() {
      this.$store.dispatch('addUser', { user: this.user })
      .then(() => {
        this.user.name.first = null
        this.user.name.last = null
        this.user.age = null

        alert('Successfully created user')
      })
    }
  }
}
</script>

テンプレートに入力フォームを設置します。入力された内容はスクリプトのデータに格納されていきます。スクリプトから入力された内容を参照するときはthis.name.firstのように書くことで参照できます。

フォームの送信ボタンが押されたらVuexストアの処理を呼び出します。$store.dispatchメソッドは、第1引数にVuexストアのactionsに定義したメソッド名を指定し、第2引数に呼び出すメソッドに渡す引数を指定します。

では次に、store/index.jsに呼び出されるメソッドを記述していきましょう。

import { v4 as uuidv4 } from 'uuid';
import firebase from '~/plugins/firebase'

const db = firebase.firestore()
const usersRef = db.collection('users')

export const actions = {
  addUser({ commit }, payload) {
    const user = {
      id: uuidv4(),
      name: payload.user.name,
      age: payload.user.age,
      created_at: firebase.firestore.FieldValue.serverTimestamp(),
      updated_at: firebase.firestore.FieldValue.serverTimestamp()
    }

    return new Promise((resolve, reject) => {
      usersRef.add(user)
      .then(ref => {
        resolve(true)
      })
      .catch(error => {
        console.error('An error occurred in addUser(): ', error)
        reject(error)
      })
    })
  }

addUserメソッドの中を順番に見ていきます。

const user = {
  id: uuidv4(),
  name: payload.user.name,
  age: payload.user.age,
  created_at: firebase.firestore.FieldValue.serverTimestamp(),
  updated_at: firebase.firestore.FieldValue.serverTimestamp()
}

idは一意な値をつけたいのでUUIDv4を使って生成した乱数をセットします。

created_atupdated_atは、上記のようにFirebase組み込みのメソッドを使ってセットします。JavaScriptのDate型だと日付型として保存されないので注意してください。

入力フォームを入力して送信ボタンを押してみます。

Firestoreにデータが追加されました。

データの取り出し

pages/users/index.vueを新規作成します。

<script>
export default {
  created() {
    this.$store.dispatch('fetchUsers')
  }
}
</script>

createdにはDOMが作成された後に実行される処理を記述します。永続化のときと同じようにVuexストアの処理を呼び出します。

store/index.jsに以下の内容を追記します。

// インポートは変更なし

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

export const actions = {
  // addUserは変更なし

  fetchUsers({ commit }) {
    commit('initUsers')

    return new Promise((resolve, reject) => {
      usersRef.orderBy('created_at', 'desc').get()
      .then(res => {
        res.forEach((doc) => {
          commit('addUsers', doc.data())
          resolve(true)
        })
      })
      .catch(error => {
        console.error('An error occurred in fetchUsers(): ', error)
        reject(error)
      })
    })
  }
}

export const mutations = {
  initUsers(state) {
    state.users = []
  },

  addUsers(state, users) {
    state.users.push(users)
  }
}

export const getters = {
  getUsers(state) {
    return state.users
  }
}

fetchUsersメソッドではまずVuexストアのデータ領域を初期化(クリア)しています。これを行わないとFirestoreから取り出したデータがどんどんデータ領域に追加されていってしまいます。

実際の初期化(クリア)処理はmutations内に定義します。Vuexストアのデータ領域は必ずmutationsに定義したメソッドを使用して操作(追加・変更・削除)する必要があります。

usersRef.orderBy('created_at', 'desc').get()

Firestoreのデータ取り出し時にソート順を指定しています。

stateはVuexストアのデータ領域です。Firestoreから取り出したデータをmutationsaddUsersメソッドを使ってstateに格納しています。

gettersstateを参照するためのメソッドを定義します。

最後にpages/index.vueに戻って、取り出したデータを表示します。

<template>
  <div class="container mt-5">

    <div class="row justify-content-center mb-3">
      <div class="col-12 col-sm-3">
        <h2>Users</h2>
      </div>
    </div>

    <div class="row justify-content-center mb-3">
      <div class="col-12 col-sm-2 font-weight-bold">Name</div>
      <div class="col-12 col-sm-1 font-weight-bold">Age</div>
    </div>

    <div v-for="(user, key) in $store.getters.getUsers" :key="key"
         class="row justify-content-center mb-3">
      <div class="col-12 col-sm-2">
        {{ user.name.first}} {{ user.name.last}}
      </div>
      <div class="col-12 col-sm-1">
        {{ user.age}}
      </div>
    </div>

  </div>
</template>

// スクリプトは変更なし

Vuexストアのデータ領域からユーザーデータを参照しています。画面を確認してみましょう。

データ永続化のときに追加したAlan Smitheeも含めてすべて取り出せています(データ部分が小さくてすみません...)。

まとめ

FirestoreはMySQLのような一般的なデータベースとは似て非なるものです。とはいえ、単にデータを永続化したいだけならデータベースとして使っても問題ないと思います。

Firestoreを使うときに注意しなければいけないことについては、また別の記事でまとめたいと思います。