🔖

CRUDアプリ(Vue CLI / Firebase)の作成について

2022/02/27に公開

1.はじめに

*急ぎの方は下記リンクからソースコードを確認すること。
https://github.com/yxo5017/crud-app-firebase-vuejs/blob/master/src/firebase/init.js

「Vue.jsとFirebaseでサクッとCRUDアプリを構築しよう」と思い、アプリ開発をしたが、エラーが発生してなかなかうまくいかず。。エラーを調査しながら進めたものの、データの表示はできるが、肝心の更新・削除・追加ができない。エラー調査中に判明したが、Firebase Javascript SDKがv8→v9へバージョンアップしたらしい。そのため、v9のコードについてあまり「やってみた系」記事が少ない模様である。

色々と記事を参考にしながら開発を進めたが、1週間経ってようやく簡易的なCRUDアプリの構築ができた。もしかしたら、v8→v9のバージョンアップにより、自分と同じように開発が滞っている人がいるのでは?と思い、記事を作成するに至った次第である。本記事では、firebaseの構築からvuejsによるCRUDアプリ開発までを解説する。

2.本記事のゴール

本記事のゴールは、firebaseとvuejsを使ったCRUDアプリを構築することがゴールである。

まず「3.Firestoreの準備」にて、firebaseでデータベースを作成する。そしてフロントエンドでデータを表示するため、作成したデータベースに対してデータを投入する。次に「4.サンプルアプリの作成」でフロントエンドの作成をする。データの表示・追加・更新・削除ができる機能を実装する。

アプリの完成イメージは下記の通り。Home画面より、shopping listのアイテムと備考を確認できる。

上部には追加ボタンがあり、追加ボタンをクリックすると新規追加の画面へ遷移する。

右側に編集ボタンと削除ボタンがあり、編集ボタンをクリックすると編集画面へ遷移する。

3.Firestoreの準備

表題の件については、下記URLに詳しく記述されているため、本記事では簡単に記載する。
https://www.harubears.com/tech/gcp-firebase/how-to-create-firestore-with-gcp-and-firebase/

まずはFirebaseのコンソール画面(https://console.firebase.google.com/) へログインすると、下記のような画面が表示される。自分の場合は、既に登録済みのプロジェクトが表示される。

そして、上記画面の「Add Project」をクリックすると下記画面が表示されるので、「My First Project」等の任意の名前をつけてプロジェクトの登録をする。

最初のFirebaseのコンソール画面(https://console.firebase.google.com/) へ戻ると、「My First Project」と表示されたプロジェクトが表示されるので、それをクリックする。

アイコンが小さくてわかりにくいが、ページ上部の「Add app」をクリックし「</>」というwebアイコンをクリックする。

下記画面へ遷移するため、任意の名前をつけてアプリを登録する。

登録後、左側の画面よりCloud Firestoreへ遷移する。

Cloud Firestoreに遷移したら、データの入力をする。今回は「shoppingLists」と言うコレクションを作成する。作成したコレクションの中で、下記の通りデータを作成する。データ作成にあたり、データタイプはstringと指定する。
item: test
note: test

最後に、アプリへデータを反映するために必要なソースコードをコピーする。まず設定画面の全般タブをクリックして、下記画面へ遷移する。

遷移後、一番下までスクロールして、赤丸で囲った部分をコピーする。

firebase側の作業は以上となる。

4.Vue-CLIの作成

4-1.初期設定

Vue CLIの作成にあたり、所定のフォルダにて下記のコマンドを実行する。

vue init webpack logistics-system
? Project name logistics-system
? Project description A Vue.js project
? Author Yosuke Ota yxo5017@icloud.com
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? No
? Set up unit tests No
? Setup e2e tests with Nightwatch? No
? Should we run npm install for you after the project has been created? (recom
mended) npm

Vue CLIをインストール後、ターミナルで「インストールしたフォルダ」へ移動した後に、「npm install firebase」を実行する。

また、所定のフォルダへ移動後、「npm run dev」を実行することにより、ローカルホストにてアプリの立ち上げが可能か確認する。

http://localhost:8080

4-2.フォルダ構成

今回開発するアプリのフォルダ構成は下記の通り。
src/
 ├ components/
 │ └ Navbar.vue //上部のメニュータグの作成
 ├ firebare/
 │ └ init.js       //firebaseのデータベースとの連携
 ├ router/
 │ └ index.js      //Home, Add, Edit画面へのリンク作成
 ├ views/
 │ ├ AddItem.vue    //Add画面の作成
 │ ├ EditItem.vue   //Edit画面の作成
 │ └ index.vue   //Home画面の作成
 ├ App.vue
 └ main.js

4-2-1.init.js

init.js画面では、下記の通り必要なfirebaseモジュールをimportして、こちらのファイルをdbとしてexportしている。その結果、他のコンポーネントにて、データベースの操作が可能となる。

import { initializeApp } from "firebase/app";
import { getFirestore } from 'firebase/firestore';
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
  apiKey: "******",
  authDomain: "******",
  projectId: "******",
  storageBucket: "******",
  messagingSenderId: "******",
  appId: "******",
  measurementId: "******"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);

const db = getFirestore(app);
export default db

4-2-2.router.js

まず、必要なコンポーネント(Vue, Router, Index, AddItem, EditItem)をimportしている。その上で、new Routerにて、各種ページへのリンクを作成している。

import Vue from 'vue'
import Router from 'vue-router'
import Index from '../views/Index'
import AddItem from '../views/AddItem.vue'
import EditItem from '../views/EditItem.vue'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Index',
      component: Index
    },
    {
      path: '/additem',
      name: 'AddItem',
      component: AddItem
    },
    {
      path: '/edititem/:item_slug',
      name: 'EditItem',
      component: EditIng
    }
  ]
})

4-2-3.Navbar

Navbarは、<router-link to="/">より、Home画面へ戻れるようにリンクを設定している。また、<router-link to="/additem">より、Add画面へ遷移可能である。

<template>
  <div class="navbar">
    <nav class="nav-extended indigo darken-2">
      <div class="nav-content">
        <router-link to="/">
          <span class="nav-title">Shopping List</span>
        </router-link>
        <a href="" class="btn-floating btn-large halfway-fab pink">
          <router-link to="/additem">
            <i class="material-icons">add</i>
          </router-link>
        </a>
      </div>
    </nav>
  </div>
</template>

4-2-4.AddItem

<form @submit.prevent="addItem">により、アイテムの追加をする関数が実行される。

<template>
  <div class="add-item container">
    <form @submit.prevent="addItem">
      <br>
      <div class="field title">
        <label for="title">Item</label>
        <input type="text" name="item" v-model="item">
      </div>
      <div class="field title">
        <label for="title">Note</label>
        <input type="text" name="note" v-model="note">
      </div>
      <div class="field center-align">
        <button class="btn pink">Add</button>
      </div>
    </form>
  </div>
</template>

<script>
import {doc, collection, getDocs, onSnapshot, addDoc, query, orderBy, deleteDoc, setDoc} from "firebase/firestore";
import db from '@/firebase/init'

export default {
  name: 'AddItem',
  data () {
    return {
      items: [],
      item: null,
      note: null,
    }
  },

実行される関数は、フォーム画面のアイテムと備考が記載されていないと実行されない。条件を満たしていると実行され、まずはローカルのitemsへ格納される。次に、addDocでfirebaseのshoppingListsというDBを取得して、こちらへデータが格納される。データが格納されたら、Index画面へ遷移する。

  methods: {
    addItem(){
      if(this.item && this.note){
        this.items.push([this.item, this.note])
        addDoc(collection(db, "shoppingLists"), {
          item: this.item,
          note: this.note,
          timestamp: new Date()
        }).then(()=> {
          this.$router.push({ name: 'Index' })
        }).catch(err => console.error(err))
        this.item = null;
        this.note = null;
      }
    }
  }
}

4-2-5.EditItem

編集画面で更新ボタンを押すと、updateItem関数が実行される。編集画面のitemおよびnote項目には、index.vueより渡された、itemおよびnoteが表示される。

<template>
  <div class="add-item container">
    <form @submit.prevent="updateItem">
      <br>
      <div class="field title">
        <label for="title">Item</label>
        <input type="text" name="item" v-model="item">
      </div>
      <div class="field title">
        <label for="title">Note</label>
        <input type="text" name="note" v-model="note">
      </div>
      <div class="field center-align">
        <button class="btn pink">Update</button>
      </div>
    </form>
  </div>
</template>

上記で説明した通り、index.vueより渡された、itemおよびnoteが表示されるようにするため、returnにてデータ表示されるように$route.paramsの各種項目の設定がされている。

methodsにてupdateItem関数が設定されている。

  • let itemRefで更新するDBの設定をしている
  • setDocにて、更新する項目の設定をしている。例えばDBのitemはthis.itemで更新するなど
  • 更新後は、Home画面へ遷移する。
<script>
import {doc, collection, getDocs, onSnapshot, addDoc, query, orderBy, deleteDoc, setDoc} from "firebase/firestore";
import db from '@/firebase/init'

export default {
  name: 'EditItem',
  data () {
    return {
      pass: this.$route.params.item_slug,
      item: this.$route.params.item,
      note: this.$route.params.note
    }
  },
  methods: {
    async updateItem() {
      let itemRef = doc(db, "shoppingLists", this.pass);
      setDoc(itemRef, {
        item: this.item,
        note: this.note,
        timestamp: new Date()
      }).then(()=> {
        this.$router.push({ name: 'Index' })
      }).catch(err => console.error(err))
      this.item = null;
      this.note = null;
    },
  },
  created(){

  }
}
</script>

<style>
.add-item{
  padding: 10px;
}
</style>

5-2-6.index

  • <table>タグにより、データをテーブル表示している。
  • 表示するデータについては、this.itemsより取得している。
  • edit/deleteボタンについては、それぞれクリックされたら関数が実行されるように設定している
  • editについては、item_slug, item, noteをEditItem画面へ渡すように設定している。
<template>
  <div class="add-item container">
    <br>
    <table>
      <thead>
       <tr>
         <th>No.</th>
         <th>Item</th>
         <th>Note</th>
       </tr>
     </thead>
      <tbody v-for="(item, index) in this.items" :key="item[2]">
        <td>{{ index + 1 }}</td>
        <td>{{ item[0] }}</td>
        <td>{{ item[1] }}</td>
        <router-link :to="{ name: 'EditItem', params: {item_slug: item[2], item: item[0], note: item[1]}}">
          <i class="material-icons edit">edit</i>
        </router-link>
        <i class="material-icons delete" @click="deleteItem(item[2])">delete</i>
      </tbody>
    </table>
  </div>
</template>

<script src="https://www.gstatic.com/firebasejs/9.1.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.1.0/firebase-firestore.js"></script>
  • deleteItem関数は、deleteDocによりデータを削除している。その後、filterによりローカル側でもデータを削除している。
  • getItemsはテーブルへデータ表示させるための関数。データ表示をするため、this.itemsへデータを格納している。
  • また、アプリが立ち上がったときにgetItemsが実行されるように、createdで関数を実行している。
<script>
import {doc, collection, getDocs, onSnapshot, addDoc, query, orderBy, deleteDoc, setDoc} from "firebase/firestore";
import db from '@/firebase/init'


export default {
  name: 'Index',

  data () {
    return {
      items: [],
      item: null,
      note: null
    }
  },
  methods: {
    async deleteItem(id){
        let request = await deleteDoc(doc(db, "shoppingLists", id));
        this.items = this.items.filter(item => {
          return item[2] != id
        })
    },
    async getItems(db) {
      const querySnapshot = await getDocs(collection(db, "shoppingLists"));
      querySnapshot.forEach((doc) => {
        this.items.push([doc.data().item, doc.data().note, doc.id]);
      });
    }
  },
  created(){
    this.getItems(db);

  }
}
</script>

<style>
.add-item{
  padding: 20px;
  max-width: 500px;
}
</style>

5.終わりに

今回はfirebaseとvuejsを使った簡単なアプリを構築した。今後も別のフレームワークを使用した簡単なCRUDアプリについて投稿する予定なので、何かリクエストがあればコメントをいただきたい。

Discussion