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

1. 始める前に

この Codelab では、Firebase を Next.js ウェブアプリ「Food 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 に関する知識

学習内容

  • Next.js App ルーターとサーバーサイド レンダリングで Firebase を使用する方法。
  • 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/friendlyeats-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. [Enter your project name] テキスト ボックスに「FriendlyEats Codelab」(または任意のプロジェクト名)と入力し、[続行] をクリックします。
  3. [Firebase 料金プランの確認] モーダルで、プランが Blaze であることを確認し、[プランを確認] をクリックします。
  4. この Codelab では Google アナリティクスは必要ないため、[このプロジェクトで Google アナリティクスを有効にする] オプションをオフにします。
  5. [プロジェクトの作成] をクリックします。
  6. プロジェクトがプロビジョニングされるのを待ってから、[続行] をクリックします。
  7. Firebase プロジェクトで [プロジェクトの設定] に移動します。後で必要になるため、プロジェクト ID をメモしておきます。この一意の識別子によって、プロジェクトが識別されます(Firebase CLI など)。

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

Firebase App Hosting と Cloud Storage for Firebase を使用するには、Firebase プロジェクトが従量課金制(Blaze)料金プランを利用している必要があります。これは、Cloud 請求先アカウントにリンクされていることを意味します。

  • Cloud 請求先アカウントには、クレジット カードなどのお支払い方法が必要です。
  • Firebase と Google Cloud を初めて使用する場合は、$300 分のクレジットと無料トライアル用の Cloud 請求先アカウントの利用条件をご確認ください。
  • この Codelab をイベントの一環として実施する場合は、利用可能な 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. [データベースを作成] をクリックします。
  3. [データベース ID] は (default) のままにします。
  4. データベースのロケーションを選択し、[Next] をクリックします。
    実際のアプリでは、ユーザーに近いロケーションを選択します。
  5. [テストモードで開始] をクリックします。セキュリティ ルールに関する免責条項を読みます。
    この Codelab の後半では、データを保護するためのセキュリティ ルールを追加します。データベースのセキュリティ ルールを追加せずに、アプリを配布または公開しないでください。
  6. [作成] をクリックします。

Cloud Storage for Firebase を設定する

  1. Firebase コンソールの左側のパネルで [ビルド] を開き、[Storage] を選択します。
  2. [開始] をクリックします。
  3. デフォルトの Storage バケットのロケーションを選択します。
    US-WEST1US-CENTRAL1US-EAST1 のバケットでは、Google Cloud Storage の「Always Free」階層を利用できます。他のすべてのロケーションのバケットは、Google Cloud Storage の料金と使用量に従います。
  4. [テストモードで開始] をクリックします。セキュリティ ルールに関する免責条項を確認します。
    この Codelab の後半で、セキュリティ ルールを追加してデータを保護します。Storage バケットのセキュリティ ルールを追加せずに、アプリを配布または公開しないでください。
  5. [作成] をクリックします。

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 ルーターを使用する 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 バックエンドを作成する

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

このセクションの終わりまでに、GitHub のリポジトリに接続された App Hosting バックエンドが作成されます。このバックエンドは、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 の設定と構成] ペインで [アプリを追加] をクリックし、コードの角かっこアイコン をクリックして新しいウェブアプリを登録します。
  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. commit メッセージ「Adding Google Authentication」で commit を作成し、GitHub リポジトリに push します。1. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  2. ウェブアプリでページを更新し、[Sign in with Google] をクリックします。ウェブアプリは更新されないため、ログインが成功したかどうかが不明確です。

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

認証状態をサーバーに渡すために、サービス ワーカーを使用します。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 データベース] ページで、[レストラン] を選択します。レストラン コレクションには、レストランを表す最上位のドキュメントが表示されます。
  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]);

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

  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. コミット メッセージ「Listen for arealtime レストランの更新情報」を含む 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 サーバー アクションには、フォームデータにアクセスするための便利な API が用意されています。たとえば、data.get("text") はフォーム送信ペイロードからテキスト値を取得します。

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 メッセージを付けて 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 アプリに機能と機能を追加する方法について学びました。具体的には、以下を使用しました。

詳細