とあるプロジェクトの dependabot の firebase の PR がコケてました。
Cannot find module 'firebase' or its corresponding type declarations.
と思ったらいつのまにか v9 が出ていて、調べてみたら書き方がえらい変わっていたので備忘録を書きます。
公式の移行ガイド:https://firebase.google.com/docs/web/modular-upgrade
v9 の利点
Modular といわれる使い方をすれば「必要なものだけ読み込む」ことができて、アプリのサイズを減らすことができるようです(Tree Shaking)。
段階的な変更
v9 を導入したからといって全部コードを書き直さないといけないかというとそういうわけでもなく、
-
import
文の部分だけ変える - v8 の書き方と v9 の Modular の書き方を共存させる
- v9 の Modular に完全移行
の3つのオプションが状況に応じてとれるようです。
1. import 文の部分だけ変える
公式の移行ガイドにもありますが
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
を
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
import 'firebase/compat/firestore';
に変えればとりあえずは動くらしいです。Moduler に対して Compat (Compatibleの略)という書かれ方をしています。時間がない人はこちらを。
3. v9 の Modular に完全移行
便宜上、先に 3. を。Modular の書き方は公式の移行ガイドによると
import firebase from "firebase/compat/app";
import "firebase/compat/auth";
const auth = firebase.auth();
auth.onAuthStateChanged(user => {
// Check for user status
});
を
import { getAuth, onAuthStateChanged } from "firebase/auth";
const auth = getAuth(firebaseApp);
onAuthStateChanged(auth, user => {
// Check for user status
});
に書き直して、とあります。
-
firebase.auth()
->getAuth(firebaseApp)
-
auth.onAuthStateChanged(user => {})
->onAuthStateChanged(auth, user => {})
ドット・チェーンで呼び出していたメソッドをすべて、それぞれのモジュールから直で import して書き直すのを地道にやっていけばいいです。
2. v8 の書き方(Compat)と v9 の Modular の書き方を共存させる
共存も可能で、例えば v8 に依存しているライブラリを使っている場合、そのライブラリには Compat のモジュールを渡して、それ以外では Modular を渡すなどできます。
色々省略していますが、私は以下のように使っています。
import firebase from 'firebase/compat/app'
import 'firebase/compat/firestore'
import { getFirestore } from 'firebase/firestore'
const config = {
apiKey: ...
}
const getApp = () => (firebase.apps && firebase.apps[0]) || firebase.initializeApp(config)
getApp()
const firestore = getFirestore(getApp())
const oldFirestore = firebase.firestore()
export { firestore, oldFirestore }
firestore
は Modular の書き方をして、 oldFirestore
は v8 にしか対応していないライブラリに渡します。
共存のときに注意しないといけないのは、全部 Modular にしない場合は初期化コードは Compat のものを使わないといけないようです。
import firebase from "firebase/compat/app"
firebase.initializeApp({ /* config */ });
import { initializeApp } from "firebase/app"
const firebaseApp = initializeApp({ /* config */ });
変更したところ・注意点
実際に中身をどう変更したか、基本 firestore.doc('posts/1').get()
-> getDoc(doc(firestore, 'posts/1'))
とか単純な変換なので、つまずいたところ・気になったところだけ。TypeScript 使用の場合です。
型
import firebase from 'firebase'
const hoge = (timestamp: firebase.firestore.Timestamp) => {}
import { Timestamp } from 'firebase/firestore'
const hoge = (timestamp: Timestamp) => {}
みたいに直で型を import できるようになった気がします。
Functions
const deleteUser = functions.httpsCallable('deleteUser')
import { httpsCallable } from 'firebase/functions'
const deleteUser = httpsCallable<{ email: string }, { userId: string }>(
functions,
'deleteUser'
)
httpsCallable<{ パラメーターの型 }, { レスポンスの型 }>()
のように型を指定できるようになった気がします、v8から出来てたらすみません。他にも型が強化されてるらしいです。
Firestore
自動生成IDを先に作るやつ
const postRef = firestore.collection(POSTS).doc()
import { collection, doc } from 'firebase/firestore'
const postRef = doc(collection(firestore, POSTS))
FieldValue
const postRef = firestore.doc("posts/1")
postRef.update({
hage: firebase.firestore.FieldValue.arrayUnion('foo'),
hoge: firebase.firestore.FieldValue.delete()
})
import { doc, updateDoc, arrayUnion, deleteField } from 'firebase/firestore'
const postRef = doc(firestore, "posts/1"))
updateDoc(postRef, {
hage: arrayUnion('foo'),
hoge: deleteField()
})
予約語衝突回避のために delete()
が deleteField()
になっていたり、若干の変更がありました。
exists
firestore.doc('posts/1').get()
.then((snap: DocumentSnapshot) => {
if (snap.exists) {
}
})
import { doc, getDoc } from 'firebase/firestore'
getDoc(doc(firestore, 'posts/1'))
.then((snap: DocumentSnapshot) => {
if (snap.exists()) {
}
})
地味に exists
が関数になってました。
batch
const batch = firestore.batch()
batch.set(postRef, { hoge: "foo" })
batch.commit()
import { writeBatch } from 'firebase/firestore'
const batch = writeBatch(firestore)
batch.set(postRef, { hoge: "foo" })
batch.commit()
クエリ
公式にも例が出てましたが。
firestore
.collection('posts')
.where('created', '<', endsWith.toDate())
.orderBy('created', 'desc')
.limit(20)
.get()
import {
query,
orderBy,
limit,
where,
collection,
getDocs,
} from 'firebase/firestore'
getDocs(
query(
collection(firestore, 'posts'),
where('created', '<', endsWith.toDate()),
orderBy('created', 'desc'),
limit(20)
)
)
Auth
ソーシャルログイン
実際に使ってるコードは違いますが、同じメールアドレスで、Google でログインしていたユーザーが Twitter でログインしたみたいなときの処理の例。結構色々変えないとでした。
auth.signInWithPopup(provider)
.then((result) => {
// 変更なし
})
.catch(function(error) {
if (error.code === 'auth/account-exists-with-different-credential') {
const pendingCred = error.credential;
const email = error.email;
auth.fetchSignInMethodsForEmail(email).then(function(methods) {
const provider = getProviderForProviderId(methods[0]); // これは自前実装
auth.signInWithPopup(provider).then(function(result) {
result.user.linkAndRetrieveDataWithCredential(pendingCred).then(function(usercred) {
goToApp(); // これは自前実装
});
});
});
}
});
import { getAuth, signInWithPopup, GoogleAuthProvider, fetchSignInMethodsForEmail, linkWithCredential } from "firebase/auth";
const auth = getAuth();
signInWithPopup(auth, provider)
.then((result) => {
// 変更なし
}).catch((error) => {
if (error.code === 'auth/account-exists-with-different-credential') {
const pendingCred = GoogleAuthProvider.credentialFromError(error)
const email = error.customData?.email as string
fetchSignInMethodsForEmail(auth, email).then(function(methods) {
const provider = getProviderForProviderId(methods[0]); // これは自前実装
signInWithPopup(auth, provider).then(function(result) {
linkWithCredential(result.user, pendingCred).then(function(usercred) {
goToApp(); // これは自前実装
});
});
});
}
});
クレデンシャルの取得、メールアドレスの場所、リンクするメソッドが主に変わってました。
ログインのコールバック
auth.signInAnonymously().then((user) => {
console.log(user.uid)
})
import { signInAnonymously } from "firebase/auth";
signInAnonymously(auth).then((userCredential) => {
console.log(userCredential.user.uid)
})
地味に user を挟まないといけなくなってました。
Storage
task = storage.ref().child(`hoge.jpg`).put(file)
task.on(
'state_changed',
() => {},
(e) => {
console.log(e)
},
() => {
task.snapshot.ref.getDownloadURL().then((url) => {
console.log(url)
})
}
)
import {
getDownloadURL,
ref,
uploadBytes,
} from 'firebase/storage'
uploadBytes(ref(storage, `hoge.jpg`), file)
.then((result) => {
getDownloadURL(result.ref).then((url) => {
console.log(url)
})
})
基本が UploadTask ではなく Promise を返すようになったようです。
uploadBytesResumable
を使えば、従来通り UploadTask が返ってくるくるようです。 uploadStringResumable
はないようですが...