VueでFirestoreのSubscribe機能を使いたい時のアプローチ
はじめに
VueでFirestoreのSubscribe機能を使ってリアルタイム処理を行いたい時に、いくつかアプローチがあるのですが、それをそれぞれ検証してどうだったかを記録として残しています。
同じように、Vue・FirestoreでSubscribe機能を使いたい人の参考になれば嬉しいです。
今回、検証に使用した環境は以下です。
- Nuxt.js v2.4.0
- TypeScriptは未使用
FirestoreのSubscribe機能について
- Firestoreで、データの変更があった時に変更差分をクライアント側に通知させる際に、
onSnapshot
メソッドを使用します。
大まかな手順として、
- リアルタイム検知したいFirestoreのドキュメントを指定して、監視対象として登録
2. Vueであれば、mounted時に登録すればよい
3. Firestoreのドキュメント・コレクションあたりを知らない場合は、こちらを参照されたし
4. https://firebase.google.com/docs/firestore/data-model?hl=ja - 別の画面への遷移などのタイミングで、subscribe登録したFirestoreのドキュメントを監視対象からはずす(unsubscribeする)
4. Vueであれば、beforeDestroy時に登録を外せばよい
前提となるデータ
- 以下のようなTODOタスクのデータが、Firestoreにあることを前提とします。
- Collectionが
todos
、title
,content
,completed
というキーバリューを持ったDocumentがある状態です
- Collectionが
各アプローチの検証・解説
Vueコンポーネントのmount・unmountフック時にFirestoreのsubscriptionを実装する
メリット
- Vueだけで完結する一番手軽なアプローチ
デメリット
- 他の画面でも同じようにsubscribeしたい場合は、実装が重複する可能性があり、DRYでない
例:未完了TODOタスクに何か変更があったときに変更差分を検知したい場合
<script>
import firebase from 'firebase/app'
import 'firebase/firestore'
const db = firebase.firestore()
export default {
mounted() {
db.collection('todos')
.where('completed', '==', false)
.onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
// 変更後のデータが取得できる
console.log('change: ', change.doc.data())
})
})
}
}
</script>
このままだと変更を監視し続けるため、永遠にFirestoreとのコネクションを維持し続けてしまいます💸
subscribeする必要がなくなれば、明示的にunsubscribeしないといけません。
Vueであれば、beforeDestroy
ライフサイクルで、subscribeしているコンポーネントが不要になった時(例:他の画面への遷移時など)にunsubscribeしてやればよいでしょう。
Firestoreのunsubscribe方法は、onSnapshot
メソッドとは別にunsubscribe
メソッド的なものが存在しないので、個人的には直感的ではないかなと思います。
方法としては、onSnapshot
メソッドを関数実行するとunsubscribeすることになります。
<script>
import firebase from 'firebase/app'
import 'firebase/firestore'
const db = firebase.firestore()
export default {
data() {
return {
unsubscribe: null
}
},
mounted() {
this.unsubscribe = db.collection('todos')
.where('completed', '==', false)
.onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
console.log('change: ', change.doc.data())
})
})
},
beforeDestroy() {
this.unsubscribe()
}
}
一番愚直な方法ですが、Vueのライフサイクルメソッドだけで完結するのでシンプルではあります。
Vuexのアクションで、Firestoreのsubscriptionを実装する
- Vuexのアクションとして、Firestoreをsubscribe・unsubscribeの処理をそれぞれ追加する
TL;DR
やることは、ライフサイクルだけで完結させる最初のアプローチで実装したsubscribe実装をVuex actionに移行するだけ
メリット
- Vuexのactionにすることで、実装を一箇所にまとめることができて、他のコンポーネントでも流用できる
- Vuex actionなので、vue devtoolでデバッグしやすい
デメリット
- 画面によって、subscribeした時の挙動を変えたい場合は、Vuex actionを変更すると使用しているコンポーネント全てに影響を与えてしまう。いわゆる「共通化の罠」みたいな話ですね
- ここらへんは、デメリットというより何でもかんでもVuexに寄せればいいわけではないので、容量用法を守って良い設計していければよいと思います(投げやり)。
実装例:todo.js(Vuex module)
import firebase from 'firebase/app'
import 'firebase/firestore'
const db = firebase.firestore()
export const state = () => ({
todos: [],
unsubscribe: null
})
export const actions = {
startListener() {
this.unsubscribe = db.collection('todos')
.where('completed', '==', false)
.onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
console.log('change: ', change.doc.data())
})
})
},
stopListener() {
this.unsubscribe()
}
}
Vueコンポーネント内で、subscribe/unsubscribeのVuex actionをmount/unmount時に実行する
<script>
import { mapActions } from 'vuex'
export default {
mounted() {
this.startListener()
},
beforeDestroy() {
this.stopListener()
},
methods: {
...mapActions('todo', 'startListener', 'stopListener')
}
}
</script>
vuefire(ライブラリ)を使用する
- vuejs公式が開発しているFirebaseとVueを連携するためのライブラリ
このライブラリを使うメリットは、Vueアプリケーションで、Firestoreのsubscribe機能を使ったリアルタイム処理をしたい時は、以下の実装だけで済んでしまうことです😳
例:未完了のTODO一覧を取得しつつ、変更があればVue側も自動更新
<script>
export default {
data() {
todos: []
},
firestore: {
todos: db.collection('todos').where('completed', '==', false)
}
}
</script>
これだけでCollectionごとTODOのデータを全取得しつつ、ライブラリ側でsubscribe/unsubscribe処理をしてくれます。かなり便利!
正確には、以下のようにFirebaseのイニシャライザ処理とvuefireのfirestorePlugin
をVue本体に適用させる必要があります。
Nuxt.jsであれば、plugin内でfirebaseとvuefireの設定をするのが良いかと思います。
import firebase from 'firebase/app'
import 'firebase/firestore'
import Vue from 'vue'
import { firestorePlugin } from 'vuefire'
if (!firebase.apps.length) {
firebase.initializeApp({
apiKey: process.env.API_KEY,
authDomain: process.env.AUTH_DOMAIN,
databaseURL: process.env.DATABASE_URL,
projectId: process.env.PROJECT_ID,
storageBucket: process.env.STORAGE_BUCKET,
messagingSenderId: process.env.MESSAGING_SENDER_ID,
appId: process.env.APP_ID
})
}
Vue.use(firestorePlugin)
export const db = firebase.firestore()
const { Timestamp, GeoPoint } = firebase.firestore
export { Timestamp, GeoPoint }
db.settings({ timestampsInSnapshots: true })
ライブラリに完全お任せではなく、自前でsubscribeさせるタイミングをコントロールしたい場合も、this.$bind
やthis.$unbind
メソッドなるものがあり、エッジケースにも対応できる柔軟さを持っている良くできたライブラリだと思います。
- https://vuefire.vuejs.org/vuefire/binding-subscriptions.html#programmatic-binding
- https://vuefire.vuejs.org/vuefire/binding-subscriptions.html#unbinding-unsubscribing-to-changes
最後に
- vuefire使うことにしました✨
Discussion