整合 Firebase 與 Next.js 應用程式

1. 事前準備

在本程式碼研究室中,您將瞭解如何將 Firebase 與名為 Friendly Eats 的 Next.js 網頁應用程式整合,這是一個提供餐廳評論的網站。

Friendly Eats 網頁應用程式

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

  • 自動建構及部署:這個程式碼研究室會使用 Firebase App Hosting,在您每次推送至已設定的分支時,自動建構及部署 Next.js 程式碼。
  • 登入和登出:完成的網頁應用程式可讓您使用 Google 帳戶登入和登出。使用者登入和持續性功能完全由 Firebase 驗證管理。
  • 圖片:完成的網頁版應用程式可讓登入的使用者上傳餐廳圖片。圖片資產會儲存在 Cloud Storage for Firebase 中。Firebase JavaScript SDK 會提供上傳圖片的公開網址。接著,這個公開網址會儲存在 Cloud Firestore 中相關餐廳的文件中。
  • 評論:完成的網頁應用程式可讓登入的使用者針對餐廳發布評論,包括星級評分和文字訊息。評論資訊會儲存在 Cloud Firestore 中。
  • 篩選器:完成的網頁應用程式可讓登入使用者依類別、地點和價格篩選餐廳清單。您也可以自訂所使用的排序方法。系統會從 Cloud Firestore 存取資料,並根據所用的篩選器套用 Firestore 查詢。

事前準備

  • GitHub 帳戶
  • 具備 Next.js 和 JavaScript 相關知識

課程內容

  • 如何搭配 Next.js 應用程式路由器和伺服器端算繪使用 Firebase。
  • 如何在 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/friendlyeats-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,且版本為 13.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. 在「輸入專案名稱」文字方塊中,輸入 FriendlyEats Codelab (或您選擇的專案名稱),然後按一下「繼續」
  3. 在「確認 Firebase 計費方案」彈出式視窗中,確認方案為 Blaze,然後按一下「確認方案」
  4. 這個程式碼研究室不需要 Google Analytics,因此請關閉「啟用這項專案的 Google Analytics」選項。
  5. 按一下 [Create Project]
  6. 等待專案佈建完成,然後按一下「Continue」
  7. 在 Firebase 專案中,前往「專案設定。記下專案 ID,後續步驟將會用到。這個專屬 ID 是用來識別專案的依據 (例如,在 Firebase CLI 中)。

升級 Firebase 定價方案

如要使用 Firebase 應用程式代管和 Firebase 適用的 Cloud Storage,您的 Firebase 專案必須採用即付即用 (Blaze) 定價方案,也就是說必須連結至 Cloud Billing 帳戶

  • 您必須提供付款方式 (例如信用卡),才能建立 Cloud Billing 帳戶。
  • 如果您是 Firebase 和 Google Cloud 的新手,請確認您是否符合$300 美元的抵免額和免費試用 Cloud Billing 帳戶的資格。
  • 如果您是為了參加活動而進行這個程式碼研究室,請向活動主辦單位詢問是否有任何 Cloud 抵用金。

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

  1. 在 Firebase 控制台中,選取「升級方案」
  2. 選取 Blaze 方案。按照畫面上的指示將 Cloud Billing 帳戶連結至專案。
    如果您需要在升級過程中建立 Cloud Billing 帳戶,可能需要返回 Firebase 控制台的升級流程,才能完成升級。

將網頁應用程式新增至 Firebase 專案

  1. 前往 Firebase 專案中的專案總覽,然後按一下 e41f2efdd9539c31.png「Web」

    如果專案中已註冊應用程式,請按一下「Add app」,即可看到「Web」圖示。
  2. 在「App nickname」(應用程式暱稱) 文字方塊中輸入容易記住的應用程式暱稱,例如 My Next.js app
  3. 請勿勾選「Also set up Firebase Hosting for this app」核取方塊。
  4. 依序點選「註冊應用程式」>「下一步」>「下一步」>「繼續前往控制台」

在 Firebase 控制台中設定 Firebase 服務

設定驗證方法

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

設定 Cloud Firestore

  1. 在 Firebase 主控台的左側面板中展開「Build」,然後選取「Firestore database」
  2. 按一下 [Create database] (建立資料庫)。
  3. 將「資料庫 ID」設為 (default)
  4. 選取資料庫的位置,然後按一下「Next」
    如果是實際應用程式,請選擇距離使用者較近的位置。
  5. 按一下「以測試模式啟動」。請詳閱安全性規則免責事項。
    在本程式碼研究室的後續部分,您將新增安全性規則來保護資料。請勿發布或公開應用程式,除非您已為資料庫新增安全性規則。
  6. 按一下「建立」

設定 Cloud Storage for Firebase

  1. 在 Firebase 主控台的左側面板中,展開「Build」,然後選取「Storage」
  2. 按一下「開始使用」
  3. 選取預設儲存體值區的位置。
    US-WEST1US-CENTRAL1US-EAST1 中的值區可使用 Google Cloud Storage 的「永遠免費」方案。其他所有位置的值區都會遵循 Google Cloud Storage 的定價和用量
  4. 按一下「以測試模式啟動」。請詳閱安全性規則免責事項。
    在本程式碼研究室的後續部分,您將新增安全性規則來保護資料。請勿發布或公開應用程式,否則請為儲存空間值區新增安全規則。
  5. 按一下「建立」

4. 查看範例程式碼

在本節中,您將查看應用程式範例程式碼庫的幾個部分,並在本程式碼研究室中新增功能。

資料夾和檔案結構

下表概述了應用程式的資料夾和檔案結構:

資料夾和檔案

說明

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 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 Hosting 後端,當您將新的修訂版本推送至 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 控制台的「應用程式代管」頁面:

應用程式代管主控台的零狀態,顯示「開始使用」按鈕

  1. 按一下「開始使用」即可開始後端建立流程。請按照下列步驟設定後端:
  2. 按照第一個步驟中的提示,連結先前建立的 GitHub 存放區。
  3. 設定部署作業設定:
    1. 將根目錄保留為 /
    2. 將上線分支設為 main
    3. 啟用自動推出功能
  4. 為後端 friendlyeats-codelab 命名。
  5. 在「建立或連結 Firebase 網頁應用程式」中,從「選取現有的 Firebase 網頁應用程式」下拉式選單中,選取先前設定的網頁應用程式。
  6. 按一下「完成並部署」。過一會兒,系統就會將您導向新頁面,您可以在該頁面中查看新應用程式代管後端的狀態!
  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. 建立修訂版本,並在修訂版本訊息中加入「新增 Google 驗證」文字,然後將修訂版本推送至 GitHub 存放區。1. 在 Firebase 控制台中開啟「App Hosting」頁面,等待新版推出作業完成。
  2. 在網頁應用程式中,重新整理頁面,然後點選「使用 Google 帳戶登入」。網頁應用程式不會更新,因此無法確認登入是否成功。

將驗證狀態傳送至伺服器

為了將驗證狀態傳遞至伺服器,我們會使用服務工作者。將 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. 建立修訂版本,並設定「Show signin state」修訂訊息,然後將其推送至 GitHub 存放區。
  2. 在 Firebase 控制台中開啟「App Hosting」頁面,等待新版本推出作業完成。
  3. 驗證新的驗證行為:
    1. 在瀏覽器中重新整理網路應用程式,標頭會顯示你的顯示名稱。
    2. 請先登出再登入。頁面會即時更新,無須重新整理。您可以對其他使用者重複執行這個步驟。
    3. 選用:按一下網頁應用程式右鍵,選取「檢視網頁原始碼」,然後搜尋顯示名稱。這項資訊會顯示在伺服器傳回的原始 HTML 來源中。

7. 查看餐廳資訊

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

新增一或多個餐廳

如要將模擬餐廳資料插入本機 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 檔案中,將 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. 建立修訂版本,並在修訂版本訊息中加入「Read the list of restaurants from Firestore」(從 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. 建立修訂版本,並在修訂版本訊息中加入「Listen for realtime restaurant updates」(即時取得餐廳更新資訊),然後將修訂版本推送至 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 中,搜尋「新增文件」窗格中所需餐廳的文件,然後選取該文件。
  9. 在「開始收集資料」窗格中,選取「評分」
  10. 在「新增文件」窗格中,找出要審查的文件,確認是否已正確插入。

Firestore 模擬器中的文件

9. 從網路應用程式儲存使用者上傳的檔案

在本節中,您將新增功能,以便在登入時替換與餐廳相關聯的圖片。您可以將圖片上傳至 Firebase Storage,並在代表餐廳的 Cloud Firestore 文件中更新圖片網址。

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

  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. 應用程式代管服務與 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 中,且可供應用程式代管後端存取。

實作評論摘要元件

  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 控制台中開啟「App Hosting」頁面,等待新版推出作業完成。
  4. 開啟餐廳的頁面。頁面頂端會顯示一句話摘要,概述頁面上的所有評論。
  5. 新增評論並重新整理頁面。您應該會看到摘要變更。

11. 結論

恭喜!您已瞭解如何使用 Firebase 為 Next.js 應用程式新增功能。具體來說,您使用了以下項目:

瞭解詳情