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

1. 始める前に

この Codelab では、Firebase をレストランのレビュー ウェブサイトである Friendly Eats という Next.js ウェブアプリと統合する方法について学びます。

フレンドリーな 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/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. [Enter your project name] テキスト ボックスに「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. [Done] をクリックします。

Cloud Storage for Firebase を設定する

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

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

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

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

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

フォルダとファイル

説明

src/components

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

src/lib

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

src/lib/firebase

Firebase 固有のコードと Firebase 構成

public

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

src/app

Next.js App Router を使用したルーティング

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 バックエンドを作成する

このセクションでは、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] ペインで、[アプリを追加] をクリックします。コードかっこのアイコン をクリックして、新しいウェブアプリを登録します。
  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] をクリックします。ウェブアプリは更新されないため、ログインが成功したかどうかが不明確です。

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

認証状態をサーバーに渡すために、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. commit メッセージ「Show signin state」で 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. 「Firestore からレストランのリストを読み取る」という commit メッセージで 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 メッセージ「レストランのリアルタイム更新を聞く」を含む 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 を作成する写真」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 を作成しますGitHub リポジトリに push します
  3. Firebase コンソールで [App Hosting] ページを開き、新しいロールアウトが完了するまで待ちます。
  4. レストランのページを開きます。上部に、ページ上のすべてのレビューの要約が 1 文表示されます。
  5. 新しいレビューを追加してページを更新します。概要が変更されたことを確認します。

11. まとめ

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

詳細