ざっくりFirestore + Vue.jsの使い方

この記事は[ただの集団 Advent Calendar 2019](https://adventar.org/calendars/4054)の5日目の記事です。
( 内容が薄くて大変申し訳ありません:bow_tone1: )

# ざっくりFirestore + Vue.jsの使い方

フロントエンドでも簡単にサーバーサイド込みで実装できると噂の`Firebase`を
いじってみたので簡単にですが`Firebase`のサービスの一つ`Firestore` についてまとめてみました。

## ● Firestoreについて
`Firestore` は `Realtime Database` の進化版のようなものです。
ドキュメント指向のいわゆる `NoSQL` と言われるタイプのDBで、
MongoDBとかDynamoDBと同じカテゴリに入るやつです。

## ●  開発環境

+ Mac : Mojave ~ Catalina
+ Node.js : v10.17.0 (nodebrewでバージョン管理)
+ yarn : v1.19.1 (brewでインストール)
+ Vue CLI : v3.12.1 (globalにインストール)
+ Firebase CLI : v7.8.1 (globalにインストール)

### ■ 初期設定

社内の勉強会で使用した資料ですが、下記を参考に初期設定。

https://qiita.com/s_sh/items/f7fc5cf721e2a1328e80

https://qiita.com/s_sh/items/3ddc4a2b0f01a45ae5a6

### ■ Firebaseパッケージをインストール

```bash:terminal
# firebaseパッケージをインストール
$ yarn add firebase

# authとか使うときに便利らしいのでとりあえずいれる
$ yarn add firebaseui
```

### ■ Firebaseパッケージをインポートする

```js:/src/plugins/firebaseConfig.js
import firebase from "firebase/app";
import "firebase/firestore";
import "firebase/storage";
import "firebase/auth";

// ↓は各自異なる値になると思います
const firebaseConfig = {
  apiKey: "hogehoge",
  authDomain: "hoge.firebaseapp.com",
  databaseURL: "https://hoge.firebaseio.com",
  projectId: "hoge",
  storageBucket: "hoge.appspot.com",
  messagingSenderId: "123456789",
  appId: "hogefuga"
};

export default firebase.initializeApp(firebaseConfig);
```

```js:/src/plugins/firestore.js
// initializeしたものをimportする
import firebase from "@/plugins/firebaseConfig";
export default firebase.firestore();
```

```js:/src/plugins/cloudStorage.js
// initializeしたものをimportする
import firebase from "@/plugins/firebaseConfig";
export default firebase.storage();
```


## ● Firestoreを試す

あくまでサンプルなので、誰でも全件データが編集削除できるザル仕様になってます。
コード自体も簡略化していますので、そのままだと動かない可能性が・・・


#### ■ Create

```vue:src/views/Create.vue
<template>
<div class="create">
  <input v-model="inputData.name" type="text">
  <button @click="create">送信</button>
</div>
</template>
<script>
import Firebase from 'firebase/app'
import db from '@/plugins/firestore'
export default {
  data () {
    return {
      inputData: {
        name: '',
      }
    }
  },
  methods: {
    create () {
      db.collection('records')
        .add({
          name: this.inputData.name,
          createBy: this.$store.state.user.uid,
          createAt: Firebase.firestore.Timestamp.now(),
          updateAt: Firebase.firestore.Timestamp.now()
        })
        .then(docRef => {
          alert(`Document written with ID: ${docRef.id}`);
        })
        .catch(error => {
          alert(`Error adding document: ${error}`);
        });
    }
  }
}
</script>
```

#### ■ Read

```vue:/src/views/Read.vue
<template>
  <div class="read">
    <ul>
      <li v-for="(record, index) in records" :key="index">
        {{ record.data.name }}
      </li>
    </ul>
  </div>
</template>
<script>
import Firebase from 'firebase/app'
import db from '@/plugins/firestore'
export default {
  data () {
    return {
      records:[]
    }
  },
  methods: {
    read () {
      // とりあえず全件取得。
      db.collection('records')
        .get()
        .then(querySnapshot => {
          querySnapshot.forEach(doc => {
            this.records.push({
              id: doc.id,
              data: doc.data()
            })
          })
        })
    }
  },
  created () {
    this.read()
  }
}
</script>
```

↓取得条件を含めるとこんな感じで書けるらしい。

```vue:/src/views/Read.vue
<script>
// 前略
read () {
  db.collection('records')
    .where('createBy', '==', this.$store.state.user.uid)
    .then(querySnapshot => {
      querySnapshot.forEach(doc => {
        this.records.push({
          id: doc.id,
          data: doc.data()
        })
      })
    })
}
// 後略
</script>
```

#### ■ Update

```vue:/src/views/Update.vue
<template>
  <div class="update">
    <input v-model="recordId" type="text">
    <input v-model="editedData.name" type="text">
    <button @click="delete">更新</button>
  </div>
</template>
<script>
import Firebase from "firebase/app";
import db from "@/plugins/firestore";
export default {
  data () {
    return {
      recordId: 'hoge',
      editedData: {
        name: '',
        updateAt: Firebase.firestore.Timestamp.now()

      }
    }
  },
  methods: {
    // 指定されたIDのdataを更新する
    update () {
      db.collection('records')
        .doc(this.recordId)
        .update(this.editedData)
        .then(() => {
          alert('Document successfully updated!')
        })
        .catch(error => {
          console.log('Error updating document: ', error)
        })
    }
  }
}
</script>
```

#### ■ Delete

```vue:/src/views/Delete.vue
<template>
  <div class="delete">
    <input v-model="recordId" type="text">
    <button @click="delete">削除</button>
  </div>
</template>
<script>
import Firebase from 'firebase/app'
import db from '@/plugins/firestore'
export default {
  data () {
    return {
      recordId: 'hoge',
    }
  },
  methods: {
    // 指定されたIDのdataを削除する(物理削除)
    delete () {
      db.collection('records')
        .doc(this.recordId)
        .delete()
        .then(() => {
          alert('Document successfully deleted!');
        })
        .catch(error => {
          console.log('Error removing document: ', error)
        })
    }
  }
}
</script>
```

### おまけ

`Cloud Storage`も主題として書きたかったのですが、Update,Delete機能が間に合いませんでした。
でも折角なので載せておきます

####  ■ Create

```vue:/src/views/CreateStorage.vue
<template>
<div class="createStorage">
  <input type="file" @change="onFileChange">
  <button @click="fileSubmit()">登録</button>
</div>
</template>
<script>
import storage from '@/plugins/cloudStorage'
import db from '@/plugins/firestore'
export default {
  data  () {
    return {
      // uploadするファイルを格納
      file: [],
      // 登録用meta情報
      submitData: {
        filePath: '',
        createBy: this.$store.state.user.uid
      },
    }
  },
  methods: {
    onFileChange() {
      this.submitData.filePath = this.file.name;
    },
    fileInfoRegister(meta_data) {
      db.collection('meta')
        .add(meta_data)
        .then(docRef => {
          console.log(docRef)
        })
        .catch(error => {
          console.log(error)
        });
    },
    fileSubmit() {
      if (this.file.name) {
        // storageのrootディレクトリにファイルを保存する想定
        storage.ref()
          .child(`${this.submitData.filePath}`)
          .put(this.file)
          .then(snapshot => {
            console.log(`Uploaded a file: ${snapshot.metadata.fullPath}`)
          })
          .catch(error => {
            console.error(`${error.code}:${error.message}`)
          })
          this.fileInfoRegister(this.submitData)
          this.submitData = {}
        }
      }
    },
  }
}
</script>
```

####  ■ Read

```vue:/src/views/ReadStorage.vue
<template>
<div class="readStorage">
  <ul>
   <li v-for="(refImgUrl, index) in refImgUrls" :key="index">
      <img :src="refImgUrl">
   </li>
  </ul>
</div>
</template>
<script>
import storage from '@/plugins/cloudStorage'
import db from '@/plugins/firestore'
export default {
  data  () {
    return {
      // dbから取得したもの
      getImgUrls: [],
      // storageからrefで取得したもの
      refImgUrls: [],
    }
  },
  methods: {
    fileRead () {
      db.collection('meta')
        .where('createBy', '==', this.$store.state.user.uid)
        .then(querySnapshot => {
          querySnapshot.forEach(doc => {
            this.getImgUrls.push({
              id: doc.id,
              data: `${doc.data().parentDir}${doc.data().fileName}`
            })
          })
          this.getImgUrls.forEach(ref => {
            storage
              .ref()
              .child(ref.data)
              .getDownloadURL()
              .then(url => {
                this.refImgUrls.push(url)
              })
              .catch(error => {
                console.log(error)
              })
          })
      })
    }
  }
  created() {
    this.fileRead()
  },
}
</script>
```

## あとがき

FirebaseはJSが読み書きできれば、自分のアイディアを形にできるので触っていてとても楽しいです。
デメリットがあるとすれば、各種rulesファイルの書き方が独特だということでしょうかね。
ちなみにサンプルコードはテストモードで動かしてますので、権限周りのルールは本当にザルです。

今回は題材をFirestoreに絞りましたが、
`Cloud Storage` のリベンジと `Firebase Authentication`での権限設定
`Cloud Functions + Firebase Admin SDK`で通知設定なども機会があれば記事を書いて見ようかと思います。