13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

DenoAdvent Calendar 2021

Day 5

DenoからFirebaseを使う

Last updated at Posted at 2021-12-05

新しい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については、

それぞれ一致しないと正しく動作しないという特徴があります。(同様の問題は@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を使用し、

  1. セキュリティルールで、管理者アカウント以外の操作を禁止する
  2. サーバー側では、管理者アカウントとしてプログラム上でログインしてからDBに接続する

といった制御を行う必要があります。
以下はRealtime Databaseで設定を行った時の流れですが、Firestoreでも基本的には同じかと思われます。

1. 管理者アカウントの作成

まず、Firebase Authenticationのメール認証で、管理者アカウントを作成します。このアカウントはプログラムから操作するために使うダミーユーザーとなります。

この際、「メールアドレス」と「パスワード」には任意の文字列を設定します。これらの値は後ほどログイン処理で使います。
値は適当で構いませんが、推測されないような文字列を使いましょう。(筆者はブラウザコンソールでcrypto.randomUUID()した値を入れました。)

image.png

ユーザーを追加すると、「ユーザーID」という値がこのアカウントに対して設定されます。この値も次で使います。

2. セキュリティルールの設定

次に、Realtime Databaseのセキュリティルールを設定します。
以下のセキュリティルールの例のように設定して下さい。

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を検討してみる価値があるかもしれません。

13
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?