Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase編)

[Vue.js の主要な機能をざっくりとつかってみたときのメモ](https://qiita.com/masatomix/items/24142b084b745ce816a9)のつづき。

わたくし2-3年前まで 1.x版のAngularJS使いだったのですが、いまどきは [Vue.js](https://jp.vuejs.org/index.html) がトレンドっぽいんで、、いろいろと調査してる件です。

前回までで、LocalStorageにTodoリストを構築するところまでやったのですが、今回はそれをFirebaseなどのバックエンドサービスに置きかえてみようとおもいます。


## やってみる
[Firebase](https://firebase.google.com/?hl=ja) は、Googleがホスティングする、Webアプリ向けのバックエンドサービスです。OAuthによる認可機能や、(オブジェクトの)データベース機能Firestoreを提供します。


あ、Firestoreのセットアップはいろんなヒトが記事にしているので、そこは省略。。
いわゆる

```javascript

  var config = {
    apiKey: "## FIREBASE API KEY ###",
    authDomain: "### FIREBASE AUTH DOMAIN ###",
    databaseURL: "https://##PROJECT ID##.firebaseio.com",
    projectId: "### CLOUD FIRESTORE PROJECT ID ###",
    storageBucket: "##PROJECT ID##.appspot.com",
    messagingSenderId: "YOUR-SENDER-ID"
  };
```
とかまではできてる前提ですすめます。

- [FirestoreとVue.jsで連絡先管理アプリを作ってみる](https://qiita.com/takato0903/items/ecd18322a50e1728ad4b)
- [firebaseのチュートリアルで、Error: Missing or insufficient permissions.と言われた時の解決法メモ](https://qiita.com/hibohiboo/items/7af59b6d0d9df98d96c4)

などが参考になりました。

**-- 2019/09/01 追記--**
サインアップやapikeyの取得について、やっぱり追加で記事を書くことにしました。
[Firebase や Google Cloud Platformの初回のサインアップその他の備忘メモ](https://qiita.com/masatomix/items/791699b3de3486618dd7)
**-- 2019/09/01 追記以上--**




### ライブラリのインストール
まずはプロジェクトにfirebaseライブラリを追加します。

```bash
$ npm install --save firebase
```

### ライブラリの初期化
さきのconfig情報をもったファイルを作成します。

```javascript:src/firebaseConfig.js(新規)
export default {
  apiKey: "## FIREBASE API KEY ###",
  authDomain: "### FIREBASE AUTH DOMAIN ###",
  databaseURL: "https://##PROJECT ID##.firebaseio.com",
  projectId: "### CLOUD FIRESTORE PROJECT ID ###",
  storageBucket: "##PROJECT ID##.appspot.com",
  messagingSenderId: "YOUR-SENDER-ID"
}
```

つぎに最初に読み込まれるmain.jsで、さきのconfigをつかいつつ、ライブラリを初期化します。

```javascript:src/main.js(修正)
import Vue from 'vue'
import App from './App'
import router from './router'
import firebase from 'firebase'  // 追加
import firebaseConfig from '@/firebaseConfig' // 追加

// if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig) // 初期化処理追加
// }

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})
```

### 検索
さて、あとはToDoリストを表示する画面側で、ライブラリを使用してCRUD操作すればOK。たとえば検索は

```javascript:HelloWorld.vueのscript部
<script>
import firebase from 'firebase' // firebaseをimportして

export default {
  name: 'HelloWorld',
  data () {
    return {
      newTask: '',
      todos: [],
      db: firebase.firestore() // firestoreを取得
    }
  },
  created: function () {
    const me = this
    const ref = this.db.collection('todos') // DBへの参照を取得して、
    ref.get().then(querySnapshot => { //getは全件取得。で、callbackで todos配列にデータをpushする
      this.loading = false
      querySnapshot.forEach(doc => { // このdocは、idとdata()を持ってて、
        const task = doc.data() //このtaskが1オブジェクト(1レコード)。
        task.id = doc.id // doc.idで、オブジェクトのキー値が取得できる(オブジェクトのキー値もオブジェクトにもたせてる)
        me.todos.push(task) // todos配列にデータをpushする
      })
    })
  },
...
</script>
```
このようにデータが取得できます。

今回は全件取得しましたが、いわゆるwhere句になる検索方法の詳細は以下を参考に。。

- [Cloud Firestore でデータを取得する](https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja)



### 更新
次に更新です。UIでタスク完了のチェックを操作したときに、下記メソッドを呼び出しています。

```javascript:HelloWorld.vueのhtml部
<ul>
  <li v-for='todo in todos' :key='todo.id'>
    <input type='checkbox' v-model='todo.isDone' @click='done(todo)' >
    <span v-bind:class='{done: todo.isDone}'>{{todo.name}}</span>
    <span @click='deleteTask(todo.id)' class='xButton'>[x]</span>
    </li>
</ul>
```

```javascript:HelloWorld.vueのscript部
done: function(todo){ 
  this.db
    .collection("todos")
    .doc(todo.id)  // key指定でオブジェクトを取得し
    .set(todo) // 新しいオブジェクトにさしかえ
},
```

### 追加

追加はこんな感じ。追加ボタンを押したときに、下記メソッドを呼び出しています。

```javascript
addTask: function () {
  const me = this
  const task = {
    id: '',
    name: this.newTask,
    isDone: false
  }
  this.db
    .collection('todos') 
    .add(task) // addで、オブジェクトを渡して更新
    .then(function (docref) {
      task.id = docref.id 
      me.todos.push(task)
    })
  this.newTask = ''
},
```
追加完了後のコールバックで戻ってくるdocref変数は、検索時の
```querySnapshot.forEach(doc => {...}``` のdocと同じモノですかね。


### 削除

削除も簡単。キー指定してdeleteするだけ。

```javascript
deleteTask: function (key) {
  ... 省略
  this.db
    .collection('todos')
    .doc(key)
    .delete()
},
```

### HelloWorld.vue 全体はこんな感じ
全体です。

```vuejs:HelloWorld.vue
<template>
  <div class="hello">
    <h1>
      <button @click="deleteEndTask">完了タスクの削除</button>My Todo Task<span class='info'>({{remainingTask.length}}/{{todos.length}})</span></h1>
    <ul>
      <li v-for='todo in todos' :key='todo.id'>
        <input type='checkbox' v-model='todo.isDone' @click='done(todo)' >
        <span v-bind:class='{done: todo.isDone}'>{{todo.name}}</span>
        <span @click='deleteTask(todo.id)' class='xButton'>[x]</span>
        </li>
    </ul>
    <form @submit.prevent='addTask'>
      <input type='text' v-model='newTask'>
      <input type='submit' value='追加' >
    </form>
  </div>
</template>

<script>
import firebase from 'firebase'

export default {
  name: 'HelloWorld',
  data () {    // 画面で使用する変数を定義する場所
    return {
      newTask: '',
      todos: [],
      db: firebase.firestore()
    }
  },
  created: function () {
    // this.todos = JSON.parse(localStorage.getItem("todos")) || [];
    const me = this
    const ref = this.db.collection('todos')
    ref.get().then(querySnapshot => {
      this.loading = false
      querySnapshot.forEach(doc => {
        const task = doc.data()
        task.id = doc.id
        me.todos.push(task)
      })
    })
  },
  methods: {
    addTask: function () {
      const me = this
      const task = {
        id: '',
        name: this.newTask,
        isDone: false
      }
      this.db
        .collection('todos')
        .add(task)
        .then(function (docref) {
          task.id = docref.id
          me.todos.push(task)
        })
      this.newTask = ''
    },
    done: function(todo){ 
      this.db
        .collection("todos")
        .doc(todo.id)
        .set(todo)
    },
    deleteTask: function (key) {
      this.todos.forEach((todo, index) => {
        if (todo.id == key) {
          this.todos.splice(index, 1)
        }
      })
      this.db
        .collection('todos')
        .doc(key)
        .delete()
    },
    deleteEndTask: function () {
      const doneTasks = this.todos.filter(todo => {
        return todo.isDone
      })
      
      this.todos = this.remainingTask

      for (let task of doneTasks) {
        this.db
          .collection('todos')
          .doc(task.id)
          .delete()
      }
    }
  },
  computed: {
    remainingTask: function () {
      return this.todos.filter(function (todo) {
        return !todo.isDone
      })
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
  font-size: 16px;
  border-bottom: 1px solid #ddd;
  padding: 16px 0;
}

h1 > button {
  float: right;
}

.xButton {
  cursor: pointer;
  font-size: 12px;
  color: red;
}

li > span.done {
  text-decoration: line-through;
  color: #bbb;
}

.info {
  color: #bbb;
  font-size: 12px;
}
</style>

```

## ソースコード
https://github.com/masatomix/todo-examples/tree/770a0fa48dc7846f65cbd34751b5ce2b31eceb54
(微妙に異なってますがほぼ同じ)


## 2019/01/21追記。データの変更通知を受け取る
[Cloud Firestore でリアルタイム アップデートを入手する](https://firebase.google.com/docs/firestore/query-data/listen?hl=ja) にあるように、Firestoreにはデータの変更通知を受け取る強力なコールバック機能があるようなので、それをつかうのもよさそうです。具体的には、

```javascript
 const ref = this.db.collection('todos')
 // 変更データを取得するメソッドを使用する。
 // ココは、refのデータに変更があった場合コールバックされる
 // querySnapshot.docChanges() に変更データが入ってる
 ref.onSnapshot(querySnapshot => {
   querySnapshot.docChanges().forEach(change => {
     // 変更されたレコード(todosの1データに相当)
     const task = change.doc.data()

     // キー値が一致してるヤツを探す
     const index = this.todos.findIndex(todo => todo.id === change.doc.id);

     // change.typeで処理を切り替え
     switch (change.type) {
       case "added":
         // ... addしたときの処理と、初回の処理(todos配列にpushとか)
         break
       case "modified":
         break
       case "removed":
         break
       default:
         break
     }
   })
 })
```
って変更を検知できます。[先のリンク先](https://firebase.google.com/docs/firestore/query-data/listen?hl=ja)の説明にも「最初のクエリ スナップショットには、クエリに一致する既存のすべてのドキュメントの added イベントが含まれています」とありますが、この機構は親切にも、最初にすべてのデータのaddedイベントを発生させてくれるので、(全件とって〜などの)初期処理を特別に記述する、などが不要になるよう考慮されています。

### やってみる

たとえば createdを以下のようにしてみます。

```javascript:HelloWorld.vue(createdを抜粋)
  created: function () {
    const me = this // thisに寄せちゃってイイかも(中途半端になっちゃった)
    const ref = this.db.collection('todos')
    // 変更データを取得するメソッドを使用する。
    // ココは、refのデータに変更があった場合コールバックされる
    // querySnapshot.docChanges() に変更データが入ってる
    ref.onSnapshot(querySnapshot => {
      querySnapshot.docChanges().forEach(change => {
        // 変更されたレコード
        const task = change.doc.data()
        console.log("task: "+ JSON.stringify(task)+" "+change.type)
       
        // 変更されたレコードの配列上のインデックス番号を特定する
        // 配列のfindIndexで、todos配列のうち todo.id プロパティがchange.doc.idとおなじヤツのIndex番号を取得
        const index = this.todos.findIndex(todo => todo.id === change.doc.id);
        console.log("index: " + index)
 
        // change.typeで処理を切り替え
        switch (change.type) {
          case "added":
            // added は、初期表示時と、データをaddしたとき
            // idが""でないときは初期表示なので単純push
            if (task.id !== "") {
              me.todos.push(task)
            }else{
              // なにもしない(addTaskメソッドでpush済み)
            }
            break

          case "modified":
            // modifiedは修正。修正が飛んでくるけどindexがないのは他画面での修正かもなのでpush
            if (index === -1) {
              // 修正(modified)で見つからないと言うことは、別画面での追加。
              this.todos.push(task)
            } else {
              // indexが見つかるときは、該当行を更新すればいいが、
              // me.todos[index] = change.doc.data()
              // これはVue.jsがViewを更新してくれない
              // me.$forceUpdate() // これかもしくは$setをつかう
              this.$set(me.todos, index, task)
            }
            break
          case "removed":
            // removedは削除
            this.todos.splice(index, 1)
            break
          default:
            break
        }
      })
    })
  },
```

追加・変更・削除後の処理をコールバックに記述したので、既存の addTask(追加)、done(更新)、deleteTask(削除)は、下記の通りとってもシンプルになります。

```javascript:HelloWorld.vue(done(あらためtoggle))
//更新
toggle: function(key){
  let target ={}
  const ref = this.db.collection('todos').doc(key) // キー指定して
  ref.get().then(docref=>{
    target = docref.data()
    target.isDone = !target.isDone //値をtoggleして
    ref.set(target) // 更新
  })
},
```


```javascript:HelloWorld.vue(addTask)
//追加
addTask: function () {
  const task = {
    id: '',
    name: this.newTask,
    isDone: false
  }
  this.todos.push(task)
  const ref = this.db.collection('todos')
  ref.add(task).then( docref => {
      task.id = docref.id
      ref.doc(docref.id).set(task) // idを入れて再度更新
    })
  this.newTask = ''
},
```

Firestoreで採番されたidをプロパティに持たせる処理がゴニョゴニョ書いてあるせいで、追加はそんな変わってなかったですねorz。

```javascript:HelloWorld.vue(deleteTask)
//削除
deleteTask: function (key) {
  this.db.collection('todos').doc(key)
    .delete()
},
```


あらためて HelloWorld.vue はこんな感じになりました。

```vuejs:HelloWorld.vue
<template>
  <main class="container">
    <h1>
      My Todo Task<span class='info'>({{remainingTask.length}}/{{todos.length}})</span>
      <span class='info' style='cursor:pointer' @click='checkAll()' v-if='!isAllChecked()'>すべてチェック/はずす</span>
      <span class='info' style='cursor:pointer' @click='unCheckAll()' v-if='isAllChecked()'>すべてチェック/はずす</span>
      <button size="sm" variant="secondary" @click="deleteEndTask">完了タスクの削除</button></h1>
    <ul>
      <li v-for='todo in todos' :key='todo.id'>
        <input type='checkbox' v-model='todo.isDone' @click='toggle(todo.id)' >
        <span v-bind:class='{done: todo.isDone}'>{{todo.name}}</span>
        <span @click='deleteTask(todo.id)' class='xButton'>[x]</span>
        </li>
    </ul>
    <form @submit.prevent='addTask'>
      <input type='text' v-model='newTask' placeholder="タスクを入力" >
      <button type='submit' variant="primary" style='margin:4px'>追加</button>
    </form>
  </main>
</template>


<script>
import firebase from 'firebase'

export default {
  name: 'HelloWorld',
  data () {    // 画面で使用する変数を定義する場所
    return {
      newTask: '',
      todos: [],
      db: firebase.firestore()
    }
  },
  created: function () {
    // this.todos = JSON.parse(localStorage.getItem("todos")) || [];
    const me = this
    const ref = this.db.collection('todos')
    // 変更データを取得するメソッドを使用する。
    // ココは、refのデータに変更があった場合コールバックされる
    // querySnapshot.docChanges() に変更データが入ってる
    ref.onSnapshot(querySnapshot => {
      querySnapshot.docChanges().forEach(change => {
        // 変更されたレコード
        const task = change.doc.data()
        console.log("task: "+ JSON.stringify(task)+" "+change.type)
       
        // 変更されたレコードの配列上のインデックス番号を特定する
        // 配列のfindIndexで、todos配列のうち todo.id プロパティがchange.doc.idとおなじヤツのIndex番号を取得
        const index = this.todos.findIndex(todo => todo.id === change.doc.id);
        console.log("index: " + index)
 
        // change.typeで処理を切り替え
        switch (change.type) {
          case "added":
            // added は、初期表示時と、データをaddしたとき
            // idが""でないときは初期表示なので単純push
            if (task.id !== "") {
              me.todos.push(task)
            }else{
              // なにもしない(addTaskメソッドでpush済み)
            }
            break

          case "modified":
            // modifiedは修正。修正が飛んでくるけどindexがないのは他画面での修正かもなのでpush
            if (index === -1) {
              // 修正(modified)で見つからないと言うことは、別画面での追加。
              this.todos.push(task)
            } else {
              // indexが見つかるときは、該当行を更新すればいいが、
              // me.todos[index] = change.doc.data()
              // これはVue.jsがViewを更新してくれない
              // me.$forceUpdate() // これかもしくは$setをつかう
              this.$set(me.todos, index, task)
            }
            break
          case "removed":
            // removedは削除
            this.todos.splice(index, 1)
            break
          default:
            break
        }
      })
    })
  },
  methods: {
    addTask: function () {
      const task = {
        id: '',
        name: this.newTask,
        isDone: false
      }
      this.todos.push(task)
      const ref = this.db.collection('todos')
      ref.add(task).then( docref => {
          task.id = docref.id
          ref.doc(docref.id).set(task) // idを入れて再度更新
        })
      this.newTask = ''
    },
    toggle: function(key){
      let target ={}
      const ref = this.db.collection('todos').doc(key)
      ref.get().then(docref=>{
        target = docref.data()
        target.isDone = !target.isDone
        ref.set(target)
      })
    },

    deleteTask: function (key) {
      this.db
        .collection('todos')
        .doc(key)
        .delete()
    },
    deleteEndTask: function () {
      const doneTasks = this.todos.filter(todo => todo.isDone)

      // this.todos = this.remainingTask

      const ref = this.db.collection('todos')
      doneTasks.forEach(doneTask=> ref.doc(doneTask.id).delete())
    },

    // 以下はすべてチェック・はずす のためのロジックで、ま、不要っちゃ不要
    updateCheck: function(key,isDone){
      let target ={}
      const ref = this.db.collection('todos').doc(key)
      ref.get().then(docref=>{
        target = docref.data()
        target.isDone = isDone
        ref.set(target)
      })
    },
    check: function(key){
      this.updateCheck(key,true)
    },
    unCheck: function(key){
      this.updateCheck(key,false)
    },
    checkAll: function() {
      const ref = this.db.collection("todos")
      this.todos.forEach(todo => this.check(todo.id))
    },
    unCheckAll: function() {
      const ref = this.db.collection("todos")
      this.todos.forEach(todo => this.unCheck(todo.id))
    },
    isAllChecked: function(){
      if(this.todos.findIndex(todo=> !todo.isDone) ===-1){ 
        // 完了していないのが一つもなければ、true
        return true
      }
      // 完了していないのが一つでもあれば false
      return false
    }
  },
  computed: {
    remainingTask: function () {
      return this.todos.filter(function (todo) {
        return !todo.isDone
      })
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
  border-bottom: 1px solid #ddd;
  padding: 16px 0;
}
h1 > button {
  float: right;
}

li {
    padding: 4px;
}

.xButton {
  cursor: pointer;
  font-size: 12px;
  color: red;
}

li > span.done {
  text-decoration: line-through;
  color: #bbb;
}

.info {
  color: #bbb;
  font-size: 12px;
}

</style>
```

### 画面
さて、画面と機能はこんな感じに。動画じゃないので臨場感ないですが、別のブラウザでの処理した内容が、コールバック経由で別のブラウザにも反映されるようになりました。

あー、ちなみに[bootstrap-vue](https://bootstrap-vue.js.org/) を入れちゃったので画面イメージは微妙に変わってますが、あんまし気にしないでください :-)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/2b283930-5727-ec97-52df-e5a43e2fb5bb.png)

データの転送サイズやプロキシがある場合の影響などいろいろ気になる部分もありますが、アプリっぽくなってきました。


## あらためて上記時点のソース
[ココ](https://github.com/masatomix/todo-examples/tree/for_qiita_000) に置いておきました。


一応このタグから構築する手順を示しておきます。

```bash
$ git clone --branch for_qiita_000 https://github.com/masatomix/todo-examples.git
$ cd todo-examples/
$ npm install

src/firebaseConfig.js を自分の設定に書き換え

$ npm run dev
```

これで、http://localhost:8080/  にブラウザでアクセスできるとおもいます。

## まとめ
Firebaseの Cloud Firestore、非常にシンプルでかつ強力ですね。。

さて[つぎは、認証機能を実装](https://qiita.com/masatomix/items/5316acdb5980289e5cc2)してみます。FirebaseにはOAuthによる認可機能(いわゆるGoogleでログインとかです)が提供されているのでそれをつかってみるのと、またその認証状態を保持しておくために、オブジェクトの状態を管理するフレームワーク[Vuex](https://vuex.vuejs.org/ja/guide/)ってのがあるので、それらをつかってみます。

おつかれさまでした。

## 関連リンク

- [Vue.js の主要な機能をざっくりとつかってみたときのメモ](https://qiita.com/masatomix/items/24142b084b745ce816a9)
- [Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase編)](https://qiita.com/masatomix/items/7be14b8eb433edcfbfc7) 本記事
- [Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase認証・認可編)](https://qiita.com/masatomix/items/5316acdb5980289e5cc2)