Edited at

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

Vue.js の主要な機能をざっくりとつかってみたときのメモのつづき。

わたくし2-3年前まで 1.x版のAngularJS使いだったのですが、いまどきは Vue.js がトレンドっぽいんで、、いろいろと調査してる件です。

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


やってみる

Firebase は、Googleがホスティングする、Webアプリ向けのバックエンドサービスです。OAuthによる認可機能や、(オブジェクトの)データベース機能Firestoreを提供します。

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

いわゆる


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"
};

とかまではできてる前提ですすめます。

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


ライブラリのインストール

まずはプロジェクトにfirebaseライブラリを追加します。

$ npm install --save firebase


ライブラリの初期化

さきのconfig情報をもったファイルを作成します。


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をつかいつつ、ライブラリを初期化します。


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。たとえば検索は


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句になる検索方法の詳細は以下を参考に。。


更新

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


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>


HelloWorld.vueのscript部

done: function(todo){ 

this.db
.collection("todos")
.doc(todo.id) // key指定でオブジェクトを取得し
.set(todo) // 新しいオブジェクトにさしかえ
},


追加

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

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するだけ。

deleteTask: function (key) {

... 省略
this.db
.collection('todos')
.doc(key)
.delete()
},


HelloWorld.vue 全体はこんな感じ

全体です。


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 でリアルタイム アップデートを入手する にあるように、Firestoreにはデータの変更通知を受け取る強力なコールバック機能があるようなので、それをつかうのもよさそうです。具体的には、

 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
}
})
})

って変更を検知できます。先のリンク先の説明にも「最初のクエリ スナップショットには、クエリに一致する既存のすべてのドキュメントの added イベントが含まれています」とありますが、この機構は親切にも、最初にすべてのデータのaddedイベントを発生させてくれるので、(全件とって〜などの)初期処理を特別に記述する、などが不要になるよう考慮されています。


やってみる

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


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(削除)は、下記の通りとってもシンプルになります。


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) // 更新
})
},


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。


HelloWorld.vue(deleteTask)

//削除

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

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


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 を入れちゃったので画面イメージは微妙に変わってますが、あんまし気にしないでください :-)

image.png

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


あらためて上記時点のソース

ココ に置いておきました。

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

$ 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、非常にシンプルでかつ強力ですね。。

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

おつかれさまでした。


関連リンク