新しいJavaScriptランタイムであるDenoと、2021年8月にリリースされたFirebase JavaScript SDK v9の組み合わせを試してみたいと思います。
なお、以下のコードはdeno deployでも動作します。
この記事は2022年12月に大幅に更新されました。
2022年9月、Denoがnpmからのインポートに対応することが発表されました。
そのため以下で紹介するFirebase SDKを使う方法に代わって、今後新たな方法が出てくる可能性があります。
以下の内容は2022年12月時点の情報です。
※Denoのnpmインポートについては Denoの新機能「npmインポート」について予習する という記事で詳しく解説しています。
「Firebase JavaScript SDK」と「Firebase Admin Node.js SDK」
Firebaseの機能を利用するためのSDKには、クライアントサイド向けであるFirebase JavaScript SDKとサーバーサイド向けであるFirebase Admin Node.js SDKが存在します。両者の特徴は以下の通りです。
JavaScript SDK | Admin Node.js SDK | |
---|---|---|
実行環境 | ブラウザ | Node.js |
Admin権限 | なし | あり |
提供方法 | npm/CDN | npm |
Denoはブラウザ互換のランタイムなので、ブラウザ向けに作られているFirebase JavaScript SDKを利用します。
ちなみに、(v9ではなく)Firebase v8を利用する方法を解説した公式ドキュメントでも、ブラウザ向けSDKの利用が推奨されています。
Firebase JavaScript SDKを使う
Firebase JavaScript SDKは、2022年12月現在、https://esm.sh から使用できることが知られています。
import { initializeApp } from "https://esm.sh/firebase@9.14.0/app?deps=@firebase/app@0.9.0,@firebase/component@0.6.0,@firebase/util@1.8.0,@firebase/logger@0.4.0";
import { getDatabase } from "https://esm.sh/firebase@9.14.0/database?deps=@firebase/app@0.9.0,@firebase/component@0.6.0,@firebase/util@1.8.0,@firebase/logger@0.4.0";
GoogleのCDNからimportする方法などもあるにはあるのですが、esm.shを使用すればTypeScriptの型定義も自動で付いてくるため、使いやすいです。
import文のクエリパラメータについて
Firebase JavaScript SDKについては、
-
https://esm.sh/firebase@9.14.0/app がインポートする
@firebase/component
のバージョン -
https://esm.sh/firebase@9.14.0/auth がインポートする
@firebase/component
のバージョン -
https://esm.sh/firebase@9.14.0/detabase がインポートする
@firebase/component
のバージョン
がそれぞれ一致しないと正しく動作しないという特徴があります。(同様の問題は@firebase/component
だけでなく、@firebase/app
、@firebase/util
、@firebase/logger
にもある)
https://esm.sh にはこのような状況に対処するため、?deps=
というクエリパラメータが用意されています。このクエリパラメータを用いて依存関係のバージョンを固定することができます。
結果として、import文に書くURLはhttps://esm.sh/firebase@9.14.0/app?deps=@firebase/app@0.9.0,@firebase/component@0.6.0,@firebase/util@1.8.0,@firebase/logger@0.4.0
のようなものになります。
(このクエリパラメータはそれぞれのライブラリの最新バージョンを使用するにように設定してあります。)
Firebase Realtime Database
ブラウザ向けのドキュメントに書いてある内容が、import元を変えるだけで基本的にそのまま動きます。
import { initializeApp } from "https://esm.sh/firebase@9.14.0/app?deps=@firebase/app@0.9.0,@firebase/component@0.6.0,@firebase/util@1.8.0,@firebase/logger@0.4.0";
import {
getDatabase,
push,
ref,
set,
} from "https://esm.sh/firebase@9.14.0/database?deps=@firebase/app@0.9.0,@firebase/component@0.6.0,@firebase/util@1.8.0,@firebase/logger@0.4.0";
// firebaseコンソール画面から取得できるfirebaseConfig
const firebaseConfig = {...};
// 実環境では、writeDataが呼ばれるまでinitializeAppの呼び出しを延期するなど工夫してもいいかも
const app = initializeApp(firebaseConfig);
const database = getDatabase(app);
/** データベースへ書き込む */
export async function writeData(name: string) {
const db = await getRealtimeDatabase();
await push(ref(db, `hello/world`), name);
// await set(ref(db, `hello/woooooeld`), data);
}
TypeScriptの型もついてイイ感じです。
enableLogging
をimportして呼び出すことで、デバッグログを表示させることができます。
import {
+ enableLogging,
getDatabase,
push,
ref,
set,
} from "https://esm.sh/firebase@9.14.0/database?deps=@firebase/app@0.9.0,@firebase/component@0.6.0,@firebase/util@1.8.0,@firebase/logger@0.4.0";
// 詳細なログが表示される
+ enableLogging(console.log);
なお、このコードは処理が止まらず、永遠にデータを待ち受け続けます。
ローカル実行等、最後まで行ったら処理を止めたい場合は、firebase/app
からdeleteApp
関数をimportした上で、処理の最後に呼び出す必要があります。
firestore
firestoreからデータを読み取る際にはこのようになります。
こちらは実はまだ試せていないのですが、以下のようなコードが動くはずです。
import { initializeApp } from "https://esm.sh/firebase@9.14.0/app?deps=@firebase/app@0.9.0,@firebase/component@0.6.0,@firebase/util@1.8.0,@firebase/logger@0.4.0";
import {
doc,
getDoc,
getFirestore,
} from "https://esm.sh/firebase@9.14.0/firestore?deps=@firebase/app@0.9.0,@firebase/component@0.6.0,@firebase/util@1.8.0,@firebase/logger@0.4.0";
const firebaseConfig = {...};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const docSnap = await getDoc(doc(db, "hello", id));
セキュリティルール
firebaseではデータベースの操作に対してセキュリティルールを設定することができます。
本来であればサーバー側ではセキュリティルールに関係なくアクセスできるAdmin SDKを使うのですが、今はブラウザ向けSDKを使っているのでセキュリティルールを考慮する必要があります。
もし書き込み権限を許可したい場合、「全て許可」してしまうと、外部からデータを操作し放題になってしまいます。
これを防ぐためには、Firebase Authenticationを使用し、
- セキュリティルールで、管理者アカウント以外の操作を禁止する
- サーバー側では、管理者アカウントとしてプログラム上でログインしてからDBに接続する
といった制御を行う必要があります。
以下はRealtime Databaseで設定を行った時の流れですが、Firestoreでも基本的には同じかと思われます。
1. 管理者アカウントの作成
まず、Firebase Authenticationのメール認証で、管理者アカウントを作成します。このアカウントはプログラムから操作するために使うダミーユーザーとなります。
この際、「メールアドレス」と「パスワード」には任意の文字列を設定します。これらの値は後ほどログイン処理で使います。
値は適当で構いませんが、推測されないような文字列を使いましょう。(筆者はブラウザコンソールでcrypto.randomUUID()
した値を入れました。)
ユーザーを追加すると、「ユーザーID」という値がこのアカウントに対して設定されます。この値も次で使います。
2. セキュリティルールの設定
次に、Realtime Databaseのセキュリティルールを設定します。
以下のセキュリティルールの例のように設定して下さい。
{
"rules": {
".read": "auth.uid == '<作成した管理者のユーザーid>'",
".write": "auth.uid == '<作成した管理者のユーザーid>'"
}
}
<作成した管理者のユーザーid>
という部分には、先ほど作成したダミーの管理者ユーザーの「ユーザーID」をコピー&ペーストします。
こうすることで、管理者アカウントのメールアドレスとパスワードでログインしていない場合は、データベースを読み書きできなくなります。
この設定をしておくと、もし管理者以外のユーザーで読み書きしようとした場合はセキュリティルールでアクセスが弾かれ、Permission deniedエラーが発生します。
※フロントエンドからもデータベースにアクセスがある場合は、これに加えて適宜権限を追加してください(未検証)。
3. プログラムから管理者アカウントで認証する
データベースにアクセスする前に、先ほど作成した管理者アカウントで認証する必要があります。コードは以下のようになります。
ここで、Firebase Authenticationで登録した「メールアドレス」と「パスワード」を使用します。
先ほどemail認証で追加したので、signInWithEmailAndPassword
関数を使います。
import { initializeApp } from "https://esm.sh/firebase@9.14.0/app?deps=@firebase/app@0.9.0,@firebase/component@0.6.0,@firebase/util@1.8.0,@firebase/logger@0.4.0";
import {
getAuth,
signInWithEmailAndPassword,
} from "https://esm.sh/firebase@9.14.0/auth?deps=@firebase/app@0.9.0,@firebase/component@0.6.0,@firebase/util@1.8.0,@firebase/logger@0.4.0";
const firebaseConfig = {...};
const app = initializeApp(firebaseConfig);
// 実環境では、DBにアクセスするまで認証処理を延期すると⭕
// (top-level-awaitがコールドスタート時間に影響するため)
// (開発者用のダミーアカウントで)認証する
const FIREBASE_AUTH_EMAIL = Deno.env.get("FIREBASE_AUTH_EMAIL")!; // メールアドレス
const FIREBASE_AUTH_PASSWORD = Deno.env.get("FIREBASE_AUTH_PASSWORD")!; // パスワード
const auth = getAuth(app);
await signInWithEmailAndPassword(auth, FIREBASE_AUTH_EMAIL, FIREBASE_AUTH_PASSWORD);
// 以下、DBに対する処理
認証に使用する管理者アカウントのemailとpasswordは、機密情報なので環境変数やdotenvで管理し、Deno.env.get()
で読み込みましょう。
バージョン8を使用した公式のexampleも参考になると思います。
(Deno.env.get()を使用してemailとpasswordを取得し、メール認証しているのが分かると思います。)
まとめ
不完全ではありますが、Denoでfirebaseを動作させることができました。
注意するポイントとしては、
- Admin SDKではなくブラウザ向けのSDKを使う
- セキュリティルールに注意する
という所に気を付けて使う必要があります。
昔はesm.shからimportするとエラーが出ていたのですが、最近改善されたことでhackが必要なくなりました。
追記:Supabaseという選択肢もある
Supabaseは公式サイトで「Firebase Alternative」と書かれている通り、Firebaseの代替となるものです。
FirebaseをDenoプロジェクトで使う場合、CLIツールやCloud Functions for FirebaseなどがNode.js前提のしくみになっており、使うのが辛いところがあります。
それに対して、2022/4/1に発表されたSupabase Functionsはdeno deploy上に構築されており、Deno側との親和性が高くなっているようです。
DenoプロジェクトでFirebaseライクな機能が使いたい時は、Supabaseを検討してみる価値があるかもしれません。