整合 Firebase 與 Next.js 應用程式

1. 事前準備

在本程式碼研究室中,您將學習如何整合 Firebase 與名為「Ager Eats」的 Next.js 網頁應用程式。後者是一個提供餐廳評論的網站。

友好飲食網頁應用程式

完成的網頁應用程式會提供實用功能,示範 Firebase 如何協助您建構 Next.js 應用程式。這些功能包括:

  • 自動建構及部署:每當您推送至已設定的分支版本時,本程式碼研究室會使用 Firebase 應用程式託管功能,自動建構及部署 Next.js 程式碼。
  • 登入和登出:完成的網頁應用程式可讓你使用 Google 帳戶登入及登出,透過 Firebase 驗證完全管理使用者登入和持續性。
  • 圖片:完成的網頁應用程式可讓已登入的使用者上傳餐廳圖片。圖片資產會儲存在 Cloud Storage for Firebase 中。Firebase JavaScript SDK 會提供上傳到上傳圖片的公開網址。這個公開網址會儲存至 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 存放區

本程式碼研究室會提供應用程式的範例程式碼集,並採用 Firebase CLI。

建立 GitHub 存放區

您可以在 https://github.com/firebase/friendeats-web 找到程式碼研究室的來源。這個存放區含有適用於多個平台的範例專案。不過,本程式碼研究室只會使用 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//.gitgit@github.com:/.git。請複製這個網址。
  5. 將本機變更推送至新的 GitHub 存放區。執行下列指令,將 預留位置替換成您的存放區網址。
    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 收集資料而定,輸入 YN
  3. 在瀏覽器中選取您的 Google 帳戶,然後按一下「允許」

3. 設定 Firebase 專案

在本節中,您將設定 Firebase 專案,並將專案與 Firebase 網頁應用程式建立關聯。您也將設定範例網頁應用程式使用的 Firebase 服務。

建立 Firebase 專案

  1. Firebase 控制台,按一下「新增專案」
  2. 在「Enter your project name」文字方塊中,輸入 FriendlyEats Codelab (或您選擇的專案名稱),然後按一下「Continue」
  3. 在「Confirm Firebase billing plan」模組中,確認方案為 Blaze,然後按一下「Confirm plan」
  4. 在這個程式碼研究室中,您不需要使用 Google Analytics (分析),因此請將「為這項專案啟用 Google Analytics (分析)」選項切換為關閉。
  5. 按一下 [Create Project]
  6. 等待專案佈建完成,然後按一下「繼續」
  7. 在 Firebase 專案中,前往「專案設定。記下專案 ID,後續步驟將會用到。這個專屬 ID 是識別專案的方式,例如在 Firebase CLI 中。

升級 Firebase 定價方案

如要使用 App 託管功能,你的 Firebase 專案必須採用 Blaze 定價方案,也就是會與 Cloud Billing 帳戶建立關聯。

  • Cloud Billing 帳戶需要付款方式 (例如信用卡)。
  • 如果你是第一次使用 Firebase 和 Google Cloud,請確認自己是否符合 $300 美元的抵免額資格,並擁有免費試用 Cloud Billing 帳戶

如要將專案升級至 Blaze 方案,請按照下列步驟操作:

  1. 在 Firebase 控制台中,選擇升級方案
  2. 在對話方塊中選取 Blaze 方案,然後按照畫面上的指示將專案連結至 Cloud Billing 帳戶。
    如需建立 Cloud Billing 帳戶,可能需要回到 Firebase 控制台的升級流程完成升級。

在 Firebase 專案中新增網頁應用程式

  1. 前往 Firebase 專案中的「專案總覽」,然後點選「e41f2efdd9539c31.png 網站」

    如果專案已註冊應用程式,請按一下「新增應用程式」查看網路圖示。
  2. 在「應用程式暱稱」文字方塊中,輸入好記的應用程式暱稱,例如 My Next.js app
  3. 取消勾選「一併為這個應用程式設定 Firebase 託管」核取方塊。
  4. 依序按一下「註冊應用程式」>「下一步」>「下一步」>「繼續前往控制台」

在 Firebase 控制台中設定 Firebase 服務

設定驗證方法

  1. 在 Firebase 控制台中,前往「驗證」
  2. 按一下「開始」
  3. 在「其他供應商」欄中,依序按一下「Google」>「啟用」。
  4. 在「對專案公開名稱」文字方塊中,輸入好記的名稱,例如 My Next.js app
  5. 從「專案支援電子郵件」下拉式選單中,選取您的電子郵件地址。
  6. 按一下「儲存」

設定 Cloud Firestore

  1. 在 Firebase 控制台中,前往「Firestore」
  2. 依序點選「建立資料庫」>「下一步」>「以測試模式開始」>「下一步」
    稍後在本程式碼研究室中,您將新增安全性規則,保護資料。請勿在未為資料庫新增安全性規則的情況下,公開發行或公開應用程式。
  3. 使用預設位置或選取您所選擇的位置。
    如果是實際應用程式,建議您選擇離使用者較近的位置。請注意,這個位置之後「無法」變更,而且也會自動成為您預設 Cloud Storage 值區的位置 (下一步)。
  4. 點選「完成」

設定 Cloud Storage for Firebase

  1. 在 Firebase 控制台中,前往「儲存空間」
  2. 依序點選「開始使用」>「以測試模式開始」>「下一步」
    稍後在本程式碼研究室中,您將新增安全性規則,確保資料安全。請勿在未為 Storage 值區新增安全性規則的情況下,公開發布或公開應用程式。
  3. 由於您在上一步中設定 Firestore,因此系統應該已選取值區的位置。
  4. 點選「完成」

4. 查看範例程式碼集

在本節中,您將瞭解應用程式範例程式碼集的幾個部分,您會在本程式碼研究室中新增功能。

資料夾和檔案結構

下表提供應用程式資料夾和檔案結構的總覽:

資料夾和檔案

說明

src/components

回應篩選器、標題、餐廳詳細資料和評論的元件

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 語言服務設定

伺服器和用戶端元件

這款應用程式是使用應用程式路由器的 Next.js 網頁應用程式。整個應用程式都會使用伺服器轉譯功能。舉例來說,src/app/page.js 檔案是負責處理主頁面的伺服器元件。src/components/RestaurantListings.jsx 檔案是檔案開頭的 "use client" 指令所表示的用戶端元件。

匯入陳述式

您可能會注意到下列匯入陳述式:

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

應用程式使用 @ 符號,避免複雜的相對匯入路徑,透過路徑別名建立。

Firebase 專屬 API

所有 Firebase API 程式碼都已納入 src/lib/firebase 目錄中。接著,個別 React 元件會從 src/lib/firebase 目錄匯入包裝的函式,而不是直接匯入 Firebase 函式。

模擬資料

模擬餐廳和評論資料會包含在 src/lib/randomData.js 檔案中。這個檔案的資料會在 src/lib/fakeRestaurants.js 檔案中的程式碼彙整。

5. 建立 App Hosting 後端

在本節中,您將設定 App Hosting 後端,以便監控 Git 存放區中的分支版本。

完成本節結束時,您的 GitHub 存放區會連結至 App Managed 後端。每當您將新的修訂版本推送至 main 分支版本時,系統就會自動重新建構及推出新版應用程式。

部署安全性規則

程式碼已有一組 Firestore 和 Cloud Storage for Firebase 適用的安全性規則。部署安全性規則後,資料庫和值區中的資料就能進一步保護,避免遭到濫用。

  1. 在終端機中,將 CLI 設為使用您先前建立的 Firebase 專案:
    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 Managed Console 的零狀態,以及「開始使用」按鈕

  1. 按一下 [開始使用] 即可啟動後端建立流程。請按照下列方式設定後端:
  2. 按照第一個步驟中的提示,連結您先前建立的 GitHub 存放區。
  3. 進行部署設定:
    1. 將根目錄保留為 /
    2. 將使用中的分支版本設為 main
    3. 啟用自動推出功能
  4. 將後端命名為 friendlyeats-codelab
  5. 在「建立或連結 Firebase 網頁應用程式」中,從「選取現有的 Firebase 網頁應用程式」下拉式選單中,選取您先前設定的網頁應用程式。
  6. 按一下「Finish and 部署」。稍後系統會將您導向新頁面,讓您查看新 App Hosting 後端的狀態!
  7. 推出完成後,按一下「網域」下方的免費網域。由於 DNS 傳播,這項作業可能需要幾分鐘的時間才能開始執行。

您已部署初始網頁應用程式!每次您將新修訂版本推送至 GitHub 存放區的 main 分支版本時,就會在 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 檔案中,程式碼已叫用 signInWithGooglesignOut 函式。

  1. 建立修訂版本訊息「Add Google Authentication」,並推送至 GitHub 存放區。1. 在 Firebase 控制台中開啟應用程式託管頁面,然後等待新推出作業完成。
  2. 在網頁應用程式中重新整理頁面,然後點選「使用 Google 帳戶登入」。網頁應用程式未更新,因此無法確定登入是否成功。

將驗證狀態傳送至伺服器

為了將驗證狀態傳遞至伺服器,我們會使用 Service Worker。將 fetchWithFirebaseHeadersgetAuthIdToken 函式替換為以下程式碼:

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 狀態掛鉤來更新使用者。

驗證變更

src/app/layout.js 檔案的根版面配置會轉譯標頭,並傳遞給使用者 (如果有的話)。

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

這表示 <Header> 元件會在伺服器執行階段期間轉譯使用者資料 (如有)。如果在初次載入網頁後,在頁面生命週期中有任何驗證更新,onAuthStateChanged 處理常式會處理這些更新。

現在,請推出新版本,並驗證建構的項目。

  1. 建立含有「顯示登入狀態」的修訂版本,並推送至 GitHub 存放區。
  2. 在 Firebase 控制台中開啟應用程式託管頁面,然後等待新推出作業完成。
  3. 驗證新的驗證行為:
    1. 在瀏覽器中重新整理網頁應用程式,你的顯示名稱會顯示在標頭中。
    2. 先登出再登入一次。網頁會即時更新,不重新整理網頁。您可以對不同使用者重複這個步驟。
    3. (選用) 在網頁應用程式上按一下滑鼠右鍵,選取「檢視網頁原始碼」,然後搜尋顯示名稱。它出現在伺服器傳回的原始 HTML 原始碼中。

7. 查看餐廳資訊

網頁應用程式包含餐廳和評論的模擬資料

新增一或多間餐廳

如要將模擬餐廳資料插入本機 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 檔案中,將 applyQueryFiltersgetRestaurants 函式替換為以下程式碼:
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 餐廳清單」的修訂版本,並推送至 GitHub 存放區。
  2. 在 Firebase 控制台中開啟應用程式託管頁面,然後等待新推出作業完成。
  3. 在網頁應用程式中重新整理頁面。餐廳圖片會以圖塊的形式顯示。

確認餐廳資訊在伺服器執行時載入

使用 Next.js 架構,可能無法在伺服器執行期間或用戶端執行時間載入資料。

如要驗證餐廳資訊在伺服器執行階段能否載入,請按照下列步驟操作:

  1. 在網頁應用程式中開啟開發人員工具,並停用 JavaScript

在開發人員工具中停用 JavaScipt

  1. 重新整理網頁應用程式,但餐廳資訊仍會載入。伺服器回應中會傳回餐廳資訊。啟用 JavaScript 後,餐廳資訊會透過用戶端 JavaScript 程式碼「水分」
  2. 在開發人員工具中重新啟用 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. 建立含有「收聽即時餐廳更新資訊」的修訂版本,並推送至 GitHub 存放區。
  2. 在 Firebase 控制台中開啟應用程式託管頁面,然後等待新推出作業完成。
  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. 建立含有「允許使用者提交餐廳評論」的修訂版本,並推送至 GitHub 存放區。
  2. 在 Firebase 控制台中開啟應用程式託管頁面,然後等待新推出作業完成。
  3. 重新整理網頁應用程式,然後從首頁選取餐廳。
  4. 在餐廳的頁面上按一下 3e19beef78bb0d0e.png
  5. 選取星級評等。
  6. 撰寫評論。
  7. 按一下 [提交]。你的評論會顯示在評論清單頂端,
  8. 在 Cloud Firestore 中,搜尋「Add document」窗格,找出您評論的餐廳並選取。
  9. 在「Start collection」(開始收集) 窗格中,選取 [ratings] (評分)
  10. 在「新增文件」窗格中找出審查文件,確認文件已如預期插入。

Firestore 模擬器中的文件

9. 儲存使用者透過網頁應用程式上傳的檔案

在本節中,您可以新增功能,以便在您登入時取代與餐廳相關聯的圖片。您將映像檔上傳至 Firebase 儲存空間,然後更新 Cloud Firestore 文件中代表餐廳的圖片網址。

如要儲存使用者從網頁應用程式上傳的檔案,請按照下列步驟操作:

  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() 函式。這個函式會使用更新後的圖片網址,更新 Cloud Firestore 中現有的餐廳文件。

驗證圖片上傳功能

如要驗證圖片是否可正常上傳,請按照下列步驟操作:

  1. 建立修訂版本訊息「允許使用者變更每間餐廳的相片」,並推送至 GitHub 存放區。
  2. 在 Firebase 控制台中開啟應用程式託管頁面,然後等待新推出作業完成。
  3. 在網頁應用程式中,確認您已登入並選取餐廳。
  4. 按一下 7067eb41fea41ff0.png,然後從檔案系統上傳圖片。映像檔會留在本機環境,並上傳至 Cloud Storage。圖片上傳後會立即顯示。
  5. 前往 Firebase 的 Cloud Storage
  6. 瀏覽至代表餐廳的資料夾。您上傳的圖片已存在於資料夾中。

6cf3f9e2303c931c.png

10. 使用生成式 AI 取得餐廳評論的摘要

請在這個部分加入評論摘要功能,方便使用者快速瞭解每個人對餐廳的想法,而不必逐一閱讀每篇評論。

在 Cloud Secret Manager 中儲存 Gemini API 金鑰

  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 託管後端存取。

導入評論摘要元件

  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 統整評論摘要」的修訂版本,並推送至 GitHub 存放區。
  3. 在 Firebase 控制台中開啟應用程式託管頁面,然後等待新推出作業完成。
  4. 開啟餐廳頁面。畫面頂端應該會顯示一句話摘要說明網頁中的所有評論。
  5. 請新增評論,然後重新整理頁面。您應該會看見摘要的變更情形。

11. 結語

恭喜!您已學會如何使用 Firebase 為 Next.js 應用程式新增功能和功能。具體而言,您學到了以下內容:

瞭解詳情