Firebase を Next.js アプリと統合する

1. 始める前に

この Codelab では、Firebase を Next.js ウェブアプリ「friend Eats」(レストランのレビューサイト)と統合する方法を学びます。

フレンドリーな Eats ウェブアプリ

完成したウェブアプリには、Firebase が Next.js アプリの構築にどのように役立つかを示す便利な機能が用意されています。主な機能は次のとおりです。

  • 自動的なビルドとデプロイ: この Codelab では Firebase App Hosting を使用して、構成済みのブランチに push するたびに Next.js コードを自動的にビルドしてデプロイします。
  • ログインとログアウト: 完成したウェブアプリで Google でログイン、ログアウトできます。ユーザーのログインと永続性は、Firebase Authentication で完全に管理されます。
  • Images: 完成したウェブアプリから、ログインしているユーザーはレストランの画像をアップロードできます。画像アセットは Cloud Storage for Firebase に保存されます。Firebase JavaScript SDK は、アップロードされた画像への公開 URL を提供します。この公開 URL が Cloud Firestore 内の関連するレストランのドキュメントに保存されます。
  • レビュー: 完成したウェブアプリを使用して、ログインしたユーザーはレストランのレビューを投稿できます。レビューは評価とテキストベースのメッセージで構成されます。クチコミ情報は Cloud Firestore に保存されます。
  • フィルタ: 完成したウェブアプリで、ログインしたユーザーは、カテゴリ、場所、価格に基づいてレストランのリストをフィルタできます。使用する並べ替え方法をカスタマイズすることもできます。データは Cloud Firestore からアクセスされ、Firestore クエリは使用するフィルタに基づいて適用されます。

前提条件

  • GitHub アカウント
  • Next.js と JavaScript に関する知識

学習内容

  • Firebase で Next.js アプリルーターおよびサーバーサイド レンダリングを使用する方法
  • Cloud Storage for Firebase で画像を保持する方法。
  • Cloud Firestore データベースでデータを読み書きする方法。
  • Firebase JavaScript SDK を使用して「Google でログイン」を使用する方法。

必要なもの

  • Git
  • Node.js の最新の安定版
  • 任意のブラウザ(Google Chrome など)
  • コードエディタとターミナルがある開発環境
  • Firebase プロジェクトの作成と管理に使用する Google アカウント
  • Firebase プロジェクトを Blaze 料金プランにアップグレード可能

2. 開発環境と GitHub リポジトリを設定する

この Codelab では、アプリのスターター コードベースを提供し、Firebase CLI を使用します。

GitHub リポジトリを作成する

Codelab のソースは https://github.com/firebase/friendeats-web にあります。リポジトリには、複数のプラットフォーム用のサンプル プロジェクトが含まれています。ただし、この Codelab では nextjs-start ディレクトリのみを使用します。次のディレクトリをメモしておきます。

* `nextjs-start`: contains the starter code upon which you build.
* `nextjs-end`: contains the solution code for the finished web app.

nextjs-start フォルダを独自のリポジトリにコピーします。

  1. ターミナルを使用して、パソコン上に新しいフォルダを作成し、新しいディレクトリに移動します。
    mkdir codelab-friendlyeats-web
    
    cd codelab-friendlyeats-web
    
  2. giget npm パッケージを使用して、nextjs-start フォルダのみを取得します。
    npx giget@latest gh:firebase/friendlyeats-web/nextjs-start#master . --install
    
  3. git を使用してローカルで変更を追跡する
    git init
    
    git commit -a -m "codelab starting point"
    
    git branch -M main
    
  4. 新しい GitHub リポジトリを作成します: https://github.com/new任意の名前を付けます。
    1. GitHub から、https://github.com//.git または git@github.com:/.git のような新しいリポジトリ URL が提供されます。この URL をコピーします。
  5. ローカルでの変更を新しい GitHub リポジトリに push します。次のコマンドを実行します。 プレースホルダは、実際のリポジトリ URL に置き換えます。
    git remote add origin <your-repository-url>
    
    git push -u origin main
    
  6. GitHub リポジトリにスターター コードが表示されます。

Firebase CLI をインストールまたは更新する

次のコマンドを実行して、Firebase CLI がインストールされていることと、v13.9.0 以降であることを確認します。

firebase --version

それより前のバージョンが表示されている場合、または Firebase CLI がインストールされていない場合は、次のインストール コマンドを実行します。

npm install -g firebase-tools@latest

権限エラーが原因で Firebase CLI をインストールできない場合は、npm のドキュメントを参照するか、別のインストール オプションを使用してください。

Firebase にログインする

  1. 次のコマンドを実行して、Firebase CLI にログインします。
    firebase login
    
  2. Firebase でデータを収集するかどうかに応じて、「Y」または「N」と入力します。
  3. ブラウザで Google アカウントを選択し、[許可] をクリックします。

3. Firebase プロジェクトを設定する

このセクションでは、Firebase プロジェクトを設定し、そのプロジェクトに Firebase ウェブアプリを関連付けます。また、サンプル ウェブアプリで使用する Firebase サービスも設定します。

Firebase プロジェクトを作成する

  1. Firebase コンソールで [プロジェクトを追加] をクリックします。
  2. [プロジェクト名を入力] テキスト ボックスに「FriendlyEats Codelab」(または任意のプロジェクト名)と入力し、[続行] をクリックします。
  3. [Firebase 料金プランの確認] モーダルで、プランが Blaze であることを確認し、[プランを確認] をクリックします。
  4. この Codelab では Google アナリティクスは必要ないため、[このプロジェクトの Google アナリティクスを有効にする] オプションをオフにします。
  5. [プロジェクトの作成] をクリックします。
  6. プロジェクトがプロビジョニングされるのを待ってから、[続行] をクリックします。
  7. Firebase プロジェクトで [プロジェクトの設定] に移動します。後で必要になるため、プロジェクト ID をメモしておきます。この一意の識別子は、プロジェクトが識別される方法(Firebase CLI など)です。

Firebase 料金プランをアップグレードする

App Hosting を使用するには、Firebase プロジェクトが Blaze お支払いプラン、つまり Cloud 請求先アカウントに関連付けられている必要があります。

  • Cloud 請求先アカウントには、クレジット カードなどのお支払い方法が必要です。
  • Firebase と Google Cloud を初めて使用する場合は、$300 分のクレジットと無料トライアル用の Cloud 請求先アカウントの利用条件をご確認ください。

プロジェクトを Blaze プランにアップグレードする手順は次のとおりです。

  1. Firebase コンソールで、プランのアップグレードを選択します。
  2. ダイアログで Blaze プランを選択し、画面上の手順に沿ってプロジェクトを Cloud 請求先アカウントに関連付けます。
    Cloud 請求先アカウントを作成する必要がある場合は、Firebase コンソールのアップグレード フローに戻ってアップグレードを完了する必要があります。

Firebase プロジェクトにウェブアプリを追加する

  1. Firebase プロジェクトの [プロジェクトの概要] に移動し、e41f2efdd9539c31.png [ウェブ] をクリックします。

    プロジェクトにすでにアプリを登録している場合は、[アプリを追加] をクリックするとウェブアイコンが表示されます。
  2. [アプリのニックネーム] テキスト ボックスに、覚えやすいアプリのニックネーム(My Next.js app など)を入力します。
  3. [このアプリにも Firebase Hosting を設定する] チェックボックスをオフのままにします。
  4. [アプリの登録] > [次へ] > [次へ] > [コンソールに進む] をクリックします。

Firebase コンソールで Firebase サービスを設定する

Authentication を設定する

  1. Firebase コンソールで [認証] に移動します。
  2. [Get started] をクリックします。
  3. [その他のプロバイダ] 列で、[Google] > [有効にする] をクリックします。
  4. [プロジェクトの一般公開名] テキスト ボックスに、覚えやすい名前(My Next.js app など)を入力します。
  5. [プロジェクトのサポートメール] プルダウンから、メールアドレスを選択します。
  6. [保存] をクリックします。

Cloud Firestore を設定する

  1. Firebase コンソールで Firestore に移動します。
  2. [データベースを作成] > [次へ] > [テストモードで開始] > [次へ] をクリックします。
    この Codelab の後半では、データを保護するためのセキュリティ ルールを追加します。データベースのセキュリティ ルールを追加せずに、アプリを配布または公開しないでください。
  3. デフォルトのロケーションを使用するか、任意のロケーションを選択します。
    実際のアプリでは、ユーザーに近いロケーションを選択します。このロケーションは後で変更することはできません。また、自動的にデフォルトの Cloud Storage バケットのロケーションにもなります(次のステップ)。
  4. [完了] をクリックします。

Cloud Storage for Firebase を設定する

  1. Firebase コンソールで [ストレージ] に移動します。
  2. [使ってみる] > [テストモードで開始] > [次へ] をクリックします。
    この Codelab の後半では、データを保護するためのセキュリティ ルールを追加します。Storage バケットのセキュリティ ルールを追加せずに、アプリを配布または公開しないでください。
  3. バケットのロケーションはすでに選択されています(前のステップで Firestore を設定したため)。
  4. [完了] をクリックします。

4. スターター コードベースを確認する

このセクションでは、この Codelab で機能を追加する、アプリのスターター コードベースのいくつかの領域を確認します。

フォルダとファイルの構造

次の表に、アプリのフォルダ構造とファイル構造の概要を示します。

フォルダとファイル

説明

src/components

フィルタ、ヘッダー、レストランの詳細、レビューの React コンポーネント

src/lib

React または Next.js に必ずしもバインドされるとは限らないユーティリティ関数

src/lib/firebase

Firebase 固有のコードと Firebase の構成

public

ウェブアプリの静的アセット(アイコンなど)

src/app

Next.js アプリルーターによるルーティング

src/app/restaurant

API ルートハンドラ

package.jsonpackage-lock.json

npm を使用したプロジェクトの依存関係

next.config.js

Next.js 固有の構成(サーバー アクションは有効です)

jsconfig.json

JavaScript 言語サービス構成

サーバーとクライアントのコンポーネント

このアプリは、App Router を使用する Next.js ウェブアプリです。サーバー レンダリングはアプリ全体で使用されます。たとえば、src/app/page.js ファイルはメインページを担当するサーバー コンポーネントです。src/components/RestaurantListings.jsx ファイルはクライアント コンポーネントであり、ファイルの先頭の "use client" ディレクティブで示されます。

インポート ステートメント

次のような import ステートメントが表示されます。

import RatingPicker from "@/src/components/RatingPicker.jsx";

このアプリでは、相対インポート パスがぎこちなくならないように @ 記号を使用しています。これは、パス エイリアスによって可能になります。

Firebase 固有の API

すべての Firebase API コードは src/lib/firebase ディレクトリ内にあります。個々の React コンポーネントは、Firebase 関数を直接インポートするのではなく、src/lib/firebase ディレクトリからラップされた関数をインポートします。

モックデータ

src/lib/randomData.js ファイルには、仮のレストランとレビューのデータが含まれています。このファイルのデータは、src/lib/fakeRestaurants.js ファイル内のコードにまとめられます。

5. App Hosting バックエンドを作成する

このセクションでは、App Hosting バックエンドを設定して、Git リポジトリのブランチを監視します。

このセクションを終えると、App Hosting バックエンドが GitHub のリポジトリに接続され、main ブランチに新しい commit を push するたびに、アプリの新しいバージョンが自動的に再ビルドされてロールアウトされます。

セキュリティ ルールをデプロイする

このコードには、Firestore と Cloud Storage for Firebase 用の一連のセキュリティ ルールがすでに含まれています。セキュリティ ルールをデプロイすると、データベースとバケット内のデータの不正使用からの保護が強化されます。

  1. ターミナルで、前の手順で作成した Firebase プロジェクトを使用するように CLI を構成します。
    firebase use --add
    
    エイリアスの入力を求められたら、「friendlyeats-codelab」と入力します。
  2. これらのセキュリティ ルールをデプロイするには、ターミナルで次のコマンドを実行します。
    firebase deploy --only firestore:rules,storage
    
  3. "Cloud Storage for Firebase needs an IAM Role to use cross-service rules. Grant the new role?"」というメッセージが表示されたら、Enter キーを押して [はい] を選択します。

ウェブアプリのコードに Firebase の構成を追加する

  1. Firebase コンソールで プロジェクトの設定に移動します。
  2. [SDK setup and configuration] ペインで [Add app] をクリックし、コードかっこのアイコン をクリックして新しいウェブアプリを登録します。
  3. ウェブアプリ作成フローの最後で、firebaseConfig 変数をコピーし、そのプロパティとその値をコピーします。
  4. コードエディタで apphosting.yaml ファイルを開き、環境変数の値に Firebase コンソールの構成値を入力します。
  5. ファイル内の既存のプロパティを、コピーしたプロパティに置き換えます。
  6. ファイルを保存します。

バックエンドの作成

  1. Firebase コンソールの [App Hosting] ページに移動します。

[使ってみる] ボタンが表示されている App Hosting コンソールのゼロ状態

  1. [開始] をクリックしてバックエンド作成フローを開始します。バックエンドを次のように構成します。
  2. 最初のステップのプロンプトに従い、先ほど作成した GitHub リポジトリを接続します。
  3. デプロイを設定します。
    1. ルート ディレクトリを / のままにしておきます。
    2. ライブブランチを main に設定します。
    3. 自動ロールアウトを有効にする
  4. バックエンドに friendlyeats-codelab という名前を付けます。
  5. [Firebase ウェブアプリを作成または関連付ける] で、[既存の Firebase ウェブアプリを選択] プルダウンから、先ほど構成したウェブアプリを選択します。
  6. [完了してデプロイ] をクリックします。しばらくすると新しいページが表示され、新しい App Hosting バックエンドのステータスを確認できます。
  7. 公開が完了したら、[ドメイン] で無料のドメインをクリックします。DNS の伝播のため、処理が開始されるまでに数分かかることがあります。

これで、最初のウェブアプリがデプロイされました。GitHub リポジトリの main ブランチに新しい commit を push するたびに、Firebase コンソールで新しいビルドとロールアウトが開始され、ロールアウトが完了するとサイトが自動的に更新されます。

6. ウェブアプリに認証を追加する

このセクションでは、ウェブアプリにログインできるように、そのウェブアプリに認証を追加します。

ログイン機能とログアウト機能を実装する

  1. src/lib/firebase/auth.js ファイルで、onAuthStateChangedsignInWithGooglesignOut 関数を次のコードに置き換えます。
export function onAuthStateChanged(cb) {
	return _onAuthStateChanged(auth, cb);
}

export async function signInWithGoogle() {
  const provider = new GoogleAuthProvider();

  try {
    await signInWithPopup(auth, provider);
  } catch (error) {
    console.error("Error signing in with Google", error);
  }
}

export async function signOut() {
  try {
    return auth.signOut();
  } catch (error) {
    console.error("Error signing out with Google", error);
  }
}

このコードでは、次の Firebase API を使用します。

Firebase API

説明

GoogleAuthProvider

Google 認証プロバイダのインスタンスを作成します。

signInWithPopup

ダイアログ ベースの認証フローを開始します。

auth.signOut

ユーザーをログアウトします。

src/components/Header.jsx ファイル内のこのコードでは、すでに signInWithGoogle 関数と signOut 関数が呼び出されています。

  1. 「Google 認証の追加」という commit メッセージを含む commit を作成し、GitHub リポジトリに push します。1. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  2. ウェブアプリでページを更新し、[Sign in with Google] をクリックします。ウェブアプリは更新されないため、ログインが成功したかどうかが不明確です。

サーバーに認証状態を送信する

認証状態をサーバーに渡すために、Service Worker を使用します。fetchWithFirebaseHeaders 関数と getAuthIdToken 関数を次のコードに置き換えます。

async function fetchWithFirebaseHeaders(request) {
  const app = initializeApp(firebaseConfig);
  const auth = getAuth(app);
  const installations = getInstallations(app);
  const headers = new Headers(request.headers);
  const [authIdToken, installationToken] = await Promise.all([
    getAuthIdToken(auth),
    getToken(installations),
  ]);
  headers.append("Firebase-Instance-ID-Token", installationToken);
  if (authIdToken) headers.append("Authorization", `Bearer ${authIdToken}`);
  const newRequest = new Request(request, { headers });
  return await fetch(newRequest);
}

async function getAuthIdToken(auth) {
  await auth.authStateReady();
  if (!auth.currentUser) return;
  return await getIdToken(auth.currentUser);
}

サーバーの認証状態を読み取る

FirebaseServerApp を使用して、クライアントの認証状態をサーバー上でミラーリングします。

src/lib/firebase/serverApp.js を開き、getAuthenticatedAppForUser 関数を置き換えます。

export async function getAuthenticatedAppForUser() {
  const idToken = headers().get("Authorization")?.split("Bearer ")[1];
  console.log('firebaseConfig', JSON.stringify(firebaseConfig));
  const firebaseServerApp = initializeServerApp(
    firebaseConfig,
    idToken
      ? {
          authIdToken: idToken,
        }
      : {}
  );

  const auth = getAuth(firebaseServerApp);
  await auth.authStateReady();

  return { firebaseServerApp, currentUser: auth.currentUser };
}

認証の変更を受け取る

認証の変更を受け取る手順は次のとおりです。

  1. src/components/Header.jsx ファイルに移動する
  2. 関数全体を次の useUserSession に置き換えます。
function useUserSession(initialUser) {
	// The initialUser comes from the server via a server component
	const [user, setUser] = useState(initialUser);
	const router = useRouter();

	// Register the service worker that sends auth state back to server
	// The service worker is built with npm run build-service-worker
	useEffect(() => {
		if ("serviceWorker" in navigator) {
			const serializedFirebaseConfig = encodeURIComponent(JSON.stringify(firebaseConfig));
			const serviceWorkerUrl = `/auth-service-worker.js?firebaseConfig=${serializedFirebaseConfig}`
		
		  navigator.serviceWorker
			.register(serviceWorkerUrl)
			.then((registration) => console.log("scope is: ", registration.scope));
		}
	  }, []);

	useEffect(() => {
		const unsubscribe = onAuthStateChanged((authUser) => {
			setUser(authUser)
		})

		return () => unsubscribe()
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, []);

	useEffect(() => {
		onAuthStateChanged((authUser) => {
			if (user === undefined) return

			// refresh when user changed to ease testing
			if (user?.email !== authUser?.email) {
				router.refresh()
			}
		})
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [user])

	return user;
}

このコードは、認証状態の変化が onAuthStateChanged 関数で指定されると、React の state フックを使用してユーザーを更新します。

変更を確認する

src/app/layout.js ファイルのルート レイアウトはヘッダーをレンダリングし、可能な場合はユーザーをプロパティとして渡します。

<Header initialUser={currentUser?.toJSON()} />

つまり、<Header> コンポーネントはサーバーの実行中にユーザーデータ(利用可能な場合)をレンダリングします。最初のページ読み込み後、ページのライフサイクル中に認証の更新が発生した場合は、onAuthStateChanged ハンドラが処理します。

次に、新しいビルドをロールアウトして、ビルド内容を検証します。

  1. 「Show signin state」という commit メッセージを含む commit を作成し、GitHub リポジトリに push します。
  2. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  3. 新しい認証動作を確認します。
    1. ブラウザでウェブアプリを更新します。ヘッダーに表示名が表示されます。
    2. ログアウトしてもう一度ログインしてください。 ページは、ページを更新しなくてもリアルタイムで更新されます。この手順は複数のユーザーで繰り返すことができます。
    3. 省略可: ウェブアプリを右クリックして [ページのソースを表示] を選択し、表示名を検索します。サーバーから返される未加工の HTML ソースに表示されます。

7. レストラン情報を表示する

ウェブアプリには、レストランやレビューの疑似データが含まれています。

1 つ以上のレストランを追加

ローカルの Cloud Firestore データベースに疑似レストランのデータを挿入する手順は次のとおりです。

  1. ウェブアプリで、2cf67d488d8e6332.png > [サンプルのレストランを追加] を選択します。
  2. Firebase コンソールの [Firestore Database] ページで、[レストラン] を選択します。レストラン コレクションの最上位のドキュメントが表示されます。ドキュメントはそれぞれレストランを表しています。
  3. いくつかのドキュメントをクリックして、レストランのドキュメントのプロパティを確認します。

レストランのリストを表示する

これで、Cloud Firestore データベースに、Next.js ウェブアプリで表示できるレストランが追加されました。

データ取得コードを定義する手順は次のとおりです。

  1. src/app/page.js ファイルで <Home /> サーバー コンポーネントを見つけ、サーバーの実行時にレストランのリストを取得する getRestaurants 関数の呼び出しを確認します。次の手順で getRestaurants 関数を実装します。
  2. src/lib/firebase/firestore.js ファイルで、applyQueryFilters 関数と getRestaurants 関数を次のコードに置き換えます。
function applyQueryFilters(q, { category, city, price, sort }) {
	if (category) {
		q = query(q, where("category", "==", category));
	}
	if (city) {
		q = query(q, where("city", "==", city));
	}
	if (price) {
		q = query(q, where("price", "==", price.length));
	}
	if (sort === "Rating" || !sort) {
		q = query(q, orderBy("avgRating", "desc"));
	} else if (sort === "Review") {
		q = query(q, orderBy("numRatings", "desc"));
	}
	return q;
}

export async function getRestaurants(db = db, filters = {}) {
	let q = query(collection(db, "restaurants"));

	q = applyQueryFilters(q, filters);
	const results = await getDocs(q);
	return results.docs.map(doc => {
		return {
			id: doc.id,
			...doc.data(),
			// Only plain objects can be passed to Client Components from Server Components
			timestamp: doc.data().timestamp.toDate(),
		};
	});
}
  1. 「Read the table list from Firestore」というコミット メッセージを含む commit を作成し、GitHub リポジトリに push します。
  2. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  3. ウェブアプリでページを更新します。レストランの画像は、ページ上にタイルとして表示されます。

レストランのリスティングがサーバーの実行時に読み込まれることを確認する

Next.js フレームワークを使用すると、データがサーバーまたはクライアントサイドの実行時にいつ読み込まれるかが明確でない場合があります。

レストランのリスティングがサーバーの実行時に読み込まれることを確認する手順は次のとおりです。

  1. ウェブアプリで DevTools を開き、JavaScript を無効にします

DevTools で JavaScipt を無効にする

  1. ウェブアプリを更新します。レストランのリストは読み込まれます。レストラン情報はサーバーのレスポンスで返されます。JavaScript が有効になっている場合は、クライアントサイドの JavaScript コードによってレストラン情報がハイドレートされます。
  2. DevTools で、JavaScript を再度有効にします

Cloud Firestore スナップショット リスナーを使用してレストランの更新をリッスンする

前のセクションでは、src/app/page.js ファイルからレストランの初期セットを読み込んだ方法を確認しました。src/app/page.js ファイルはサーバー コンポーネントであり、Firebase データ取得コードを含め、サーバー上でレンダリングされます。

src/components/RestaurantListings.jsx ファイルはクライアント コンポーネントであり、サーバーでレンダリングされるマークアップをハイドレートするように構成できます。

サーバーでレンダリングされたマークアップをハイドレートするように src/components/RestaurantListings.jsx ファイルを設定する手順は次のとおりです。

  1. src/components/RestaurantListings.jsx ファイルで、あらかじめ作成された次のコードを確認します。
useEffect(() => {
        const unsubscribe = getRestaurantsSnapshot(data => {
                setRestaurants(data);
        }, filters);

        return () => {
                unsubscribe();
        };
}, [filters]);

このコードは getRestaurantsSnapshot() 関数を呼び出します。これは、前のステップで実装した getRestaurants() 関数に似ています。ただし、このスナップショット関数にはコールバック メカニズムが用意されており、レストランのコレクションが変更されるたびにコールバックが呼び出されます。

  1. src/lib/firebase/firestore.js ファイルで、getRestaurantsSnapshot() 関数を次のコードに置き換えます。
export function getRestaurantsSnapshot(cb, filters = {}) {
	if (typeof cb !== "function") {
		console.log("Error: The callback parameter is not a function");
		return;
	}

	let q = query(collection(db, "restaurants"));
	q = applyQueryFilters(q, filters);

	const unsubscribe = onSnapshot(q, querySnapshot => {
		const results = querySnapshot.docs.map(doc => {
			return {
				id: doc.id,
				...doc.data(),
				// Only plain objects can be passed to Client Components from Server Components
				timestamp: doc.data().timestamp.toDate(),
			};
		});

		cb(results);
	});

	return unsubscribe;
}

Firestore データベース ページで行った変更が、ウェブアプリにリアルタイムで反映されるようになりました。

  1. commit メッセージ「Listen for realtime レストラン updates」を含む commit を作成し、GitHub リポジトリに push します。
  2. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  3. ウェブアプリで、27ca5d1e8ed8adfe.png > [サンプルのレストランを追加] を選択します。スナップショット関数が正しく実装されていれば、ページを更新しなくてもレストランがリアルタイムで表示されます。

8. ウェブアプリでユーザーが送信したレビューを保存する

  1. src/lib/firebase/firestore.js ファイルで、updateWithRating() 関数を次のコードに置き換えます。
const updateWithRating = async (
	transaction,
	docRef,
	newRatingDocument,
	review
) => {
	const restaurant = await transaction.get(docRef);
	const data = restaurant.data();
	const newNumRatings = data?.numRatings ? data.numRatings + 1 : 1;
	const newSumRating = (data?.sumRating || 0) + Number(review.rating);
	const newAverage = newSumRating / newNumRatings;

	transaction.update(docRef, {
		numRatings: newNumRatings,
		sumRating: newSumRating,
		avgRating: newAverage,
	});

	transaction.set(newRatingDocument, {
		...review,
		timestamp: Timestamp.fromDate(new Date()),
	});
};

このコードは、新しいレビューを表す新しい Firestore ドキュメントを挿入します。このコードは、レストランを表す既存の Firestore ドキュメントも更新し、評価数と平均評価値の数値を更新します。

  1. 関数全体を次の addReviewToRestaurant() に置き換えます。
export async function addReviewToRestaurant(db, restaurantId, review) {
	if (!restaurantId) {
		throw new Error("No restaurant ID has been provided.");
	}

	if (!review) {
		throw new Error("A valid review has not been provided.");
	}

	try {
		const docRef = doc(collection(db, "restaurants"), restaurantId);
		const newRatingDocument = doc(
			collection(db, `restaurants/${restaurantId}/ratings`)
		);

		// corrected line
		await runTransaction(db, transaction =>
			updateWithRating(transaction, docRef, newRatingDocument, review)
		);
	} catch (error) {
		console.error(
			"There was an error adding the rating to the restaurant",
			error
		);
		throw error;
	}
}

Next.js サーバー アクションを実装する

Next.js のサーバー アクションには、フォーム送信ペイロードからテキスト値を取得するための data.get("text") などのフォームデータにアクセスするための便利な API が用意されています。

Next.js サーバー アクションを使用して審査フォームの送信を処理する手順は次のとおりです。

  1. src/components/ReviewDialog.jsx ファイルで、<form> 要素の action 属性を見つけます。
<form action={handleReviewFormSubmission}>

action 属性値は、次のステップで実装する関数を参照します。

  1. src/app/actions.js ファイルで、handleReviewFormSubmission() 関数を次のコードに置き換えます。
// This is a next.js server action, which is an alpha feature, so
// use with caution.
// https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions
export async function handleReviewFormSubmission(data) {
        const { app } = await getAuthenticatedAppForUser();
        const db = getFirestore(app);

        await addReviewToRestaurant(db, data.get("restaurantId"), {
                text: data.get("text"),
                rating: data.get("rating"),

                // This came from a hidden form field.
                userId: data.get("userId"),
        });
}

レストランのクチコミを追加する

レビュー送信のサポートを実装したので、レビューが Cloud Firestore に正しく挿入されていることを検証できるようになりました。

レビューを追加して Cloud Firestore に挿入されたことを確認する手順は次のとおりです。

  1. 「ユーザーにレストランのレビューの送信を許可する」という commit メッセージ付きの commit を作成し、GitHub リポジトリに push します。
  2. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  3. ウェブアプリを更新し、ホームページからレストランを選択します。
  4. レストランのページで 3e19beef78bb0d0e.png をクリックします。
  5. 評価を選択してください。
  6. クチコミを書く。
  7. [送信] をクリックします。自分が投稿したレビューは、レビュー リストの一番上に表示されます。
  8. Cloud Firestore で、[ドキュメントを追加] ペインで、レビューしたレストランのドキュメントを検索して選択します。
  9. [コレクションを開始] ペインで [評価] を選択します。
  10. [ドキュメントを追加] ペインで、確認するドキュメントを見つけて、意図したとおりに挿入されていることを確認します。

Firestore エミュレータ内のドキュメント

9. ユーザーがアップロードしたファイルをウェブアプリから保存する

このセクションでは、ログイン時にレストランに関連付けられた画像を置き換える機能を追加します。Firebase Storage に画像をアップロードし、レストランを表す Cloud Firestore ドキュメント内の画像 URL を更新します。

ユーザーがアップロードしたファイルをウェブアプリから保存する手順は次のとおりです。

  1. src/components/Restaurant.jsx ファイルで、ユーザーがファイルをアップロードしたときに実行されるコードを確認します。
async function handleRestaurantImage(target) {
        const image = target.files ? target.files[0] : null;
        if (!image) {
                return;
        }

        const imageURL = await updateRestaurantImage(id, image);
        setRestaurant({ ...restaurant, photo: imageURL });
}

変更する必要はありませんが、次の手順で updateRestaurantImage() 関数の動作を実装します。

  1. src/lib/firebase/storage.js ファイルで、updateRestaurantImage() 関数と uploadImage() 関数を次のコードに置き換えます。
export async function updateRestaurantImage(restaurantId, image) {
	try {
		if (!restaurantId)
			throw new Error("No restaurant ID has been provided.");

		if (!image || !image.name)
			throw new Error("A valid image has not been provided.");

		const publicImageUrl = await uploadImage(restaurantId, image);
		await updateRestaurantImageReference(restaurantId, publicImageUrl);

		return publicImageUrl;
	} catch (error) {
		console.error("Error processing request:", error);
	}
}

async function uploadImage(restaurantId, image) {
	const filePath = `images/${restaurantId}/${image.name}`;
	const newImageRef = ref(storage, filePath);
	await uploadBytesResumable(newImageRef, image);

	return await getDownloadURL(newImageRef);
}

updateRestaurantImageReference() 関数はすでに実装されています。この関数は、更新された画像の URL で Cloud Firestore の既存のレストラン ドキュメントを更新します。

画像のアップロード機能を確認する

画像が想定どおりにアップロードされていることを確認するには、次の手順を行います。

  1. 「ユーザーが各レストランの写真を変更できるようにする」というコミット メッセージを含む commit を作成し、GitHub リポジトリに push します。
  2. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  3. ウェブアプリで、ログインしていることを確認し、レストランを選択します。
  4. [7067eb41fea41ff0.png] をクリックして、ファイル システムから画像をアップロードします。イメージはローカル環境から出て、Cloud Storage にアップロードされます。画像はアップロード後すぐに表示されます。
  5. Firebase の Cloud Storage に移動します。
  6. レストランを表すフォルダに移動します。アップロードした画像がこのフォルダ内に存在します。

6cf3f9e2303c931c.png

10. 生成 AI を使用してレストランのレビューを要約する

このセクションでは、クチコミの概要機能を追加します。この機能により、ユーザーはすべてのクチコミを読むことなく、レストランに対する人々の感想をすばやく把握できます。

Gemini API キーを Cloud Secret Manager に保存する

  1. Gemini API を使用するには API キーが必要です。Google AI Studio でキーを作成します
  2. App Hosting は Cloud Secret Manager と統合されているため、API キーなどの機密性の高い値を安全に保存できます。
    1. ターミナルで、次のコマンドを実行して新しいシークレットを作成します。
    firebase apphosting:secrets:set gemini-api-key
    
    1. シークレットの値を求められたら、Google AI Studio から Gemini API キーをコピーして貼り付けます。
    2. 新しいシークレットを apphosting.yaml に追加するかどうかを尋ねられたら、「Y」と入力して承認します。

これで、Gemini API キーが Cloud Secret Manager に安全に保存され、App Hosting バックエンドからアクセスできるようになりました。

クチコミの概要コンポーネントを実装する

  1. src/components/Reviews/ReviewSummary.jsx で、GeminiSummary 関数を次のコードに置き換えます。
    export async function GeminiSummary({ restaurantId }) {
        const { firebaseServerApp } = await getAuthenticatedAppForUser();
        const reviews = await getReviewsByRestaurantId(
            getFirestore(firebaseServerApp),
            restaurantId
        );
    
        const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
        const model = genAI.getGenerativeModel({ model: "gemini-pro"});
    
        const reviewSeparator = "@";
        const prompt = `
            Based on the following restaurant reviews, 
            where each review is separated by a '${reviewSeparator}' character, 
            create a one-sentence summary of what people think of the restaurant. 
    
            Here are the reviews: ${reviews.map(review => review.text).join(reviewSeparator)}
        `;
    
        try {
            const result = await model.generateContent(prompt);
            const response = await result.response;
            const text = response.text();
    
            return (
                <div className="restaurant__review_summary">
                    <p>{text}</p>
                    <p>✨ Summarized with Gemini</p>
                </div>
            );
        } catch (e) {
            console.error(e);
            return <p>Error contacting Gemini</p>;
        }
    }
    
  2. 「AI を使用してレビューを要約する」という commit メッセージを含む commit を作成し、GitHub リポジトリに push します。
  3. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  4. レストランのページを開きます。上部に、ページ上のすべてのレビューの要約が 1 文表示されます。
  5. 新しいレビューを追加してページを更新します。概要の変更が表示されます。

11. まとめ

これで完了です。Firebase を使用して Next.js アプリに機能を追加する方法を学習しました。具体的には、次のものを使用しました。

詳細