Vue.js + FirebaseでTodoアプリを作る


普段はRailsを使っているのだけど、webサービス作るときの選択肢を増やすためにFirebaseを使えるようにしておこうと思った。

僕はjsのフレームワークではVue.jsが一番好きなので、今回はVue+Firebaseで簡単なTodoアプリを作ってみた。

![todo_🔊.png](https://qiita-image-store.s3.amazonaws.com/0/59008/ea53c04b-cf66-c758-5cbc-3c784840413a.png "todo_🔊.png")


今回のTodoアプリでできることは以下の通り。ものすごくシンプルだし、見た目は気にしないでほしい。

* タスクの作成
* タスクの未完了/完了での表示切り替え
* タスクの削除

ソースはgithubにあげてあるので、よかったら参考にしてください。

github
https://github.com/megaya/vue_firebase_todo

```config/local.env
'use strict'
module.exports = {
  FIRE_BASE: {
    API_KEY: '"......"',
    AUTH_DOMAIN: '"......"',
    DATABASE_URL: '"......"',
    PROJECT_ID: '"......"',
    STORAGE_BUCKET: '"......"',
    MESSAGING_SENDERID: '"......"',
  }
}
```

一応パブリックにあげてあるので`config/locale.env`というファイルにfirebaseの設定を書くようにした。もしgithubに上がっているものを使う場合は、上記の内容でファイルを作成して、`.....`部分は自分のFirebaseの値をいれてください。


## Vue.jsの環境構築

```
$ npm install --global vue-cli
$ vue init webpack todo
```

`vue-cli`という便利なものがあるので、コマンド一発で簡単にインストールできる。色々と環境設定を選択する。僕はyarnを選んでいるけど、npmでも問題ない。

```
$ yarn add firebase --save
```

インストールが終わったらfirebaseのライブラリも追加しておく。

## ソースの說明

触るのは`src/main.js`と`src/App.vue`のみ。まずはmain.jsの方から見ていく。

### src/main.js

```src/main.js
import Vue from 'vue'
import App from './App'
import firebase from 'firebase'

Vue.config.productionTip = false

console.log(process.env.FIRE_BASE);

// Initialize Firebase
var config = {
        apiKey: process.env.FIRE_BASE.API_KEY,
        authDomain: process.env.AUTH_DOMAIN,
        databaseURL: process.env.FIRE_BASE.DATABASE_URL,
        projectId: process.env.FIRE_BASE.PROJECT_ID,
        storageBucket: process.env.STORAGE_BUCKET,
        messagingSenderId: process.env.MESSAGING_SENDERID,
};
firebase.initializeApp(config);

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

特に特殊なことはやっていなくって、Firebaseの設定ファイルを読み込んでいるだけ。`config/local.env`に書かれた変数を、`config/dev.env.js`で読み込んでいる。

![vue-todo_–_概要_–_Firebase_console.png](https://qiita-image-store.s3.amazonaws.com/0/59008/a3ea0477-473e-e9d0-d05f-2304a44454a9.png "vue-todo_–_概要_–_Firebase_console.png")

公式トップページ
https://firebase.google.com/?hl=ja

FireBaseの設定値はプロジェクトを作ってすぐのページの「ウェブアプリにFirebaseを追加」を押すと表示されるので、その値をそのまま入れる。


### src/App.vue

```src/App.vue
<template>
  <div id="app">
    <h2>タスク</h2>
    <div>
      <input type="text" v-model="newTodoName">
      <button type="submit" v-on:click="createTodo()">タスク作成</button>
    </div>
    <ul>
      <li><button type="submit" v-on:click="showTodoType = 'all'">すべて</button></li>
      <li><button type="submit" v-on:click="showTodoType = 'active'">未完タスク一覧</button></li>
      <li><button type="submit" v-on:click="showTodoType = 'complete'">完了タスク一覧</button></li>
    </ul>
    <ul v-for="(todo, key) in filteredTodos">
      <li><input class="toggle" type="checkbox" v-model="todo.isComplete" v-on:click="updateIsCompleteTodo(todo, key)">{{ todo.name }}</li>
      <button type="submit" v-on:click="deleteTodo(key)">削除</button>
    </ul>
  </div>
</template>

<script>
import firebase from 'firebase'

export default {
  name: 'App',
  created: function() {
    this.database = firebase.database();
    this.todosRef = this.database.ref('todos');

    var _this = this;
    this.todosRef.on('value', function(snapshot) {
      _this.todos = snapshot.val(); // データに変化が起きたときに再取得する
    });
  },
  computed: {
    filteredTodos: function () {
      if (this.showTodoType == 'all') {
        return this.todos;
      } else {
        var showIsComplete = false;
        if (this.showTodoType == 'complete') { showIsComplete = true }
        var filterTodos = {};
        for (var key in this.todos) {
          var todo = this.todos[key];
          if (todo.isComplete == showIsComplete) { filterTodos[key] = todo; }
        }
        return filterTodos;
      }
    }
  },
  methods: {
    createTodo: function() {
      if (this.newTodoName == "") { return; }
      this.todosRef.push({
        name: this.newTodoName,
        isComplete: false,
      })
      this.newTodoName = "";
    },
    updateIsCompleteTodo: function (todo, key) {
      todo.isComplete = !todo.isComplete
      var updates = {};
      updates['/todos/' + key] = todo;
      this.database.ref().update(updates);
    },
    deleteTodo: function (key) {
      this.database.ref('todos').child(key).remove();
    },
  },

  data () {
    return {
      database: null,
      todosRef: null,
      newTodoName: '',
      showTodoType: 'all',
      todos: []
    }
  }
}
</script>

<style>
</style>

```

#### データベースから値の取得

```javascript
  created: function() {
    this.database = firebase.database();
    this.todosRef = this.database.ref('todos');

    var _this = this;
    this.todosRef.on('value', function(snapshot) {
      _this.todos = snapshot.val();
    });
  },
```

Vueはページが開かれたときに`created`が呼ばれるようになっているので、まずはここでFirebaseからタスクの一覧を取得するようにする。

Firebaseでは`this.database().ref('todos')`というように、refにパスを指定して値を取得する。`todos`以下のオブジェクトがすべて取得できる。

`this.todosRef.on('value', ...`でイベントを設定しておくと、todos以下のデータに変化が起きたときにコードが実行される。ここでは`_this.todos = snapshot.val();`が実行される。これでタスクを追加したり、更新したり、削除されたりすると、常に`this.todos`に最新のデータがロードされるようになる。


ウェブでのデータの取得（公式ページ）
https://firebase.google.com/docs/database/web/retrieve-data?hl=ja


#### タスクの追加

```html
<input type="text" v-model="newTodoName">
<button type="submit" v-on:click="createTodo()">タスク作成</button>
```

```javascript
    createTodo: function() {
      if (this.newTodoName == "") { return; }
      this.todosRef.push({
        name: this.newTodoName,
        isComplete: false,
      })
      this.newTodoName = "";
    },
```

タスクを作成するテキストボックスの部分。isCompletehという値はタスクが完了しているかどうかを判断するためのフラグ。


`created`で生成した`todoRef`に値を渡す。FirebaseのデータベースはNoSQLなので、jsonで楽にデータを入れていける。migrationとかを設定しなくてもサクサクっとできるので、プロトタイプのサイトとかを試しに作るのとかにも向いていると思う。（本格的にやるなら設計が大変だけど）

#### タスク一覧の表示


```html
    <ul v-for="(todo, key) in filteredTodos">
      <li><input class="toggle" type="checkbox" v-model="todo.isComplete" v-on:click="updateIsCompleteTodo(todo, key)">{{ todo.name }}</li>
      <button type="submit" v-on:click="deleteTodo(key)">削除</button>
    </ul>
```

`created`で取得したtodosを`v-for`でループして回す。

```
{ xxxxx: {name: 'hoge', isComplete: false}, yyyyyy: {name: 'hoge', isComplete: false}}
```

Firebaseで取得した値は配列ではなく、上記のようにオブジェクトでくる。なので`<ul v-for="(todo, key) in filteredTodos">`のように、オブジェクトのkeyも一緒にループして取得するようにしている。ちなみにオブジェクトのキーはFirebase側で勝手に作ってくれるので、こちらでデータベースのプライマリーキーのようなものを意識して作る必要は一切ない。


```html
    <ul>
      <li><button type="submit" v-on:click="showTodoType = 'all'">すべて</button></li>
      <li><button type="submit" v-on:click="showTodoType = 'active'">未完タスク一覧</button></li>
      <li><button type="submit" v-on:click="showTodoType = 'complete'">完了タスク一覧</button></li>
    </ul> 
```

```javascript
  computed: {
    filteredTodos: function () {
      if (this.showTodoType == 'all') {
        return this.todos;
      } else {
        var showIsComplete = false;
        if (this.showTodoType == 'complete') { showIsComplete = true }
        var filterTodos = {};
        for (var key in this.todos) {
          var todo = this.todos[key];
          if (todo.isComplete == showIsComplete) { filterTodos[key] = todo; }
        }
        return filterTodos;
      }
    }
```

さっき書いたようにオブジェクトでループを回しているので、タスクを並び替えるときは`Array.filtter`が使えない。オブジェクトをループして配列を作り直すようにしている。

createdのときの取得時点でオブジェクトを配列に入れ直した方がいいのかもしれない。このあたりもっといい方法があれば教えてください…

ちなみに`filteredTodos`というのを`computed`に定義しているのは値がキャッシュされるから。`methods`に定義しても同じように動作するけど、そのキャッシュ周りの動作が微妙に違うので、詳しくは下記参照。

https://jp.vuejs.org/v2/guide/computed.html


#### タスクの削除


```html
<button type="submit" v-on:click="deleteTodo(key)">削除</button>
```

```javascript
    deleteTodo: function (key) {
      this.database.ref('todos').child(key).remove();
    },
```

ここは単純にループしたkeyをもとにして削除している。ただしもっとしっかり作るなら、コールバックでしっかりエラー処理をしておくべき。（手抜き）

#### タスクの未完了/完了の変更（アップデート）

```html
<li><input class="toggle" type="checkbox" v-model="todo.isComplete" v-on:click="updateIsCompleteTodo(todo, key)">{{ todo.name }}</li>
```

```javascript
    updateIsCompleteTodo: function (todo, key) {
      todo.isComplete = !todo.isComplete
      var updates = {};
      updates['/todos/' + key] = todo;
      this.database.ref().update(updates);
    },
```

updatesというオブジェクトにパスを入れてアップデートする。updatesに複数のkeyを入れれば一斉にアップデートもできる。

ちなみにkeyを指定して、`{ isComplete: falase }`だけのオブジェクトを作って投げると、`name`カラムが消滅するので、オブジェクトをアップデートするときは、値をすべてしっかり入れるようにする。


## Firebase便利！

小さいアプリとかモック作りたいときはものすごく便利だと思う。カラムの変更が難しいので、大規模だとかなり設計をしっかりしないといけないとも感じた。

APIを自分で作らなくていいってまじで楽だ…


## 参考

FirebaseのRealtime Databaseをざっくり使ってみる ~導入から取得編(保存に関しては編集中)~
https://qiita.com/seiya1121/items/4cbc32678558315c2159


TodoMVC の例（Vue.jsの公式サイト）
https://jp.vuejs.org/v2/examples/todomvc.html