こんにちは。virapture株式会社のもぐめっとです。
cafe soultreeというところでの一枚なのですが、ドラマ撮影でも使われるくらい素敵なところなのでぜひ皆さんごはんしてみてください。
今回はtypescriptを使っているプロジェクトでFirebase js sdkのv9アップグレードをしたのですが、その時のメモや、便利関数みたいなものを紹介しようと思います。(Nuxtを使うのりで書いてはいますが、一応汎用的に使えるような内容でサンプルを置いてます。)
v9へのアップグレードのメリットについては公式に解説を譲ります。だいたいパフォーマンスよくなるってかんじです。
それではやってみましょう!
v9にアップグレードする
まずはupgradeします。ncuいれてないひとはnpm install -g npm-check-updates
でいれておきましょう。
$ ncu -u firebase
$ npm i
package.jsonをみて、9以上になってればおkです。
初期化準備
dotenv使ってenvから必要なapiKeyとか呼び出していますが、必要に応じて書き換えちゃってください。
import { FirebaseApp, getApps, initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'
const firebaseApp = getFirebaseApp()
export const firestore = getFirestore(firebaseApp)
function getFirebaseApp(): FirebaseApp {
const apps = getApps()
if (apps.length > 0) {
return apps[0]
}
return 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,
measurementId: process.env.MEASUREMENT_ID
})
}
Firestoreの書き換え例
新しい書き方はgetDoc, getDocs, updateDoc, setDoc, deleteDocといった関数を使う形の使い方になりました。
before / afterで紹介していきます。
collection add
before
import { firestore } from '~/plugins/firebase'
await firestore.collection('posts').add({title: 'タイトル'})
after
import { addDoc, collection } from 'firebase/firestore'
import { firestore } from '~/plugins/firebase'
await addDoc(collection(firestore, 'posts'), {title: 'タイトル'})
collection read
before
import { firestore } from '~/plugins/firebase'
const snapshot = await firestore.collection('posts').get()
const posts = snapshot.docs.map(doc => doc.data())
after
import { getDocs, collection } from 'firebase/firestore'
import { firestore } from '~/plugins/firebase'
const snapshot = await getDocs(collection(firestore, 'posts'))
const posts = snapshot.docs.map(doc => doc.data())
document read
before
import { firestore } from '~/plugins/firebase'
const snapshot = await firestore.doc('posts/1').get()
after
import { getDoc, doc, collection } from 'firebase/firestore'
import { firestore } from '~/plugins/firebase'
const snapshot = await getDoc(doc(firestore, 'posts/1'))
また、CollectionReferenceを定義していてそこからidを算出していた方法も記載しておきます。
before
import { firestore } from '~/plugins/firebase'
const postsCollection = firestore.collection('posts')
await postsCollection.doc('1').get()
after
import { getDoc, doc, collection } from 'firebase/firestore'
import { firestore } from '~/plugins/firebase'
const postsCollection = collection(firestore, 'posts')
await getDoc(doc(postsCollection, '1'))
document set / update
before
import { firestore } from '~/plugins/firebase'
await firestore.doc('posts/1').set({title: 'Cameconはフォトコンサイト'}, { merge: true })
await firestore.doc('posts/1').update({title: 'Cameconはフォトコンサイト'})
after
import { setDoc } from 'firebase/firestore'
await setDoc(doc(firestore, 'posts/1'), {title: 'Cameconはフォトコンサイト'}, { merge: true })
await updateDoc(doc(firestore, 'posts/1'), {title: 'Cameconはフォトコンサイト'})
条件によって追加されるQuery
値があるときだけqueryを追加したいケースがあると思うのですが、その場合の書き方はこちらになります。
before
async function fetchPosts(startDate?: Date) {
let query = this.getPostsCollection()
.where('startDate', '<=', new Date())
.orderBy('startDate', 'desc')
.limit(10)
query = startDate ? query.startAfter(startDate) : query
const snapshot = await query.get()
...
}
after
async function fetchPosts(startDate?: Date) {
let execQuery = query(
this.getPostsCollection(),
where('startDate', '<=', new Date()),
orderBy('startDate', 'desc'),
limit(10)
)
execQuery = startDate
? query(execQuery, startAfter(startDate))
: execQuery
const snapshot = await getDocs(execQuery)
...
}
query関数はQueryの型であればなんでも突っ込めるみたいなので入れ子状に再度query関数で入れ直すことによって実現してます。
Listenする(onSnapshot)
firestoreの醍醐味であるリアルタイムにデータが降ってくるonSnapshotも少し書き方がかわりました。
before
function subscribePost(): () => void {
return firestore.doc('posts/1').onSnapshot(
snapshot => {
snapshot.data()
...
}
)
}
after
import { onSnapshot, Unsubscribe } from 'firebase/firestore'
function subscribePost(postId: string): Unsubscribe {
return onSnapshot(doc(firestore, 'posts/1'),
(snapshot) => {
snapshot.data()
...
}
)
}
地味に帰り値の型が新たに定義されてますね。
死ぬほどクソ便利な型補完
この記事でも言及されてますが、DocumentReferenceやらCollectionReferenceに型も一緒に定義してあげるとなんと、もれなく型チェックしてくれます!何これ便利やん!!!!!
例えばIPostというinterfaceがあった場合の書き方を紹介します。
before
await firestore.doc('posts/1').update({title: 0}) //何も言われない
after
await updateDoc(doc(firestore, 'posts/1') as DocumentReference<IPost>, {
title: 0 // おこられる
})
コンパイルの時点で怒ってくれるので型が変わったとしてもちゃんと追従してくれるし、addのときとかは必要要素がないとaddできないとか怒ってくれるのでこれによって圧倒的に保守性がアップします。
感動しました。
ただ、注意点としてtypeとinterfaceが入り混じった複雑なものを型として定義してるとエラーになることがあったので、型はなるべくどちらかに統一して使ったほうがよさそうです。
ちなみに、readのときもdocument.data()
の帰り値が自動的に指定した型になるのでいちいちdocument.data() as IPost
とかかかなくてもすむようになります。
便利関数としてまとめてみた
毎回firestoreを入れるのも手間なので、firestore呼ばずにすむように便利関数を定義してみました。
import { FirebaseApp, getApps, initializeApp } from 'firebase/app'
import {
doc,
collection,
getFirestore,
CollectionReference,
DocumentReference,
WriteBatch,
writeBatch,
collectionGroup,
Query
} from 'firebase/firestore'
const firebaseApp = getFirebaseApp()
function getFirebaseApp(): FirebaseApp {
const apps = getApps()
if (apps.length > 0) {
return apps[0]
}
return 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,
measurementId: process.env.MEASUREMENT_ID
})
}
const firestore = getFirestore(firebaseApp) // 基本はexportせず、下記関数を通してfirestoreを使う
export function getCollection<T>(path: string): CollectionReference<T> {
return collection(firestore, path) as CollectionReference<T>
}
export function getBatch(): WriteBatch {
return writeBatch(firestore)
}
export function getCollectionGroup<T>(collectionId: string): Query<T> {
return collectionGroup(firestore, collectionId) as Query<T>
}
export function getDocument<T>(path: string): DocumentReference<T> {
return doc(firestore, path) as DocumentReference<T>
}
これ一つ作っておけば少し書くのが楽ちんになります。
import { getDoc, addDoc, updateDoc } from 'firebase/firestore'
import { getDocument, getCollection } from '~/plugins/firebase'
async function sample() {
// add
await addDoc(getCollection<IPost>('posts'), { title: 'Camecon始めよう!' })
// read
const document = getDocument<IPost>('posts/1')
const snapshot = await getDoc(document)
// update
await updateDoc(document, { title: 'https://camecon.me' })
}
まとめ
正直量が多いと移行めちゃめちゃ大変でした。なので早めに移行しましょう!
移行すればサイズが減ってパフォーマンスがあがったりだとか、補完きくようになるなど地味にいいことがあるのでやっとくと良いと思います!
今回紹介できなかった他のサンプルについてはこちらの記事がよりよくまとまっているので参考にすると良いと思います。
また、今回色んな所でfirestoreを読んでいるがゆえに移行が大変だったという経緯もあるので、なるべくfirestoreの呼び出し(だけに限らずfunctionsやauthもそうだけど)はRepository層とかにまとめておくと今後の移行コストや保守コストも下がっていくんじゃないかなと思います。