整合 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 知識

課程內容

事前準備

  • 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 add .
    
    git commit -m "Codelab starting point"
    
    git branch -M main
    
  4. 建立新的 GitHub 存放區:https://github.com/new。命名方式不限。
  5. 視您驗證 GitHub 的方式 (HTTPS 或 SSH) 而定,複製 GitHub 為您建立的新網址:
    • https://github.com/<USER_NAME>/<REPOSITORY_NAME>.git或
    • git@github.com:<USER_NAME>/<REPOSITORY_NAME>.git
  6. 執行下列指令,將本機變更推送至新的 GitHub 存放區。將 <REPOSITORY_URL> 預留位置替換為實際的存放區網址。
    git remote add origin <REPOSITORY_URL>
    
    git push -u origin main
    
  7. 現在,您應該會在 GitHub 存放區中看到範例程式碼。

安裝或更新 Firebase CLI

執行下列指令,確認您已安裝 Firebase CLI,且版本為 14.1.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. 使用上一步驟所用的 Google 帳戶登入 Firebase 控制台。
  2. 按一下按鈕建立新專案,然後輸入專案名稱 (例如 FriendlyEats Codelab)。
  3. 按一下「繼續」。
  4. 如果出現提示訊息,請詳閱並接受 Firebase 條款,然後點選「繼續」。
  5. (選用) 在 Firebase 控制台中啟用 AI 輔助功能 (稱為「Gemini in Firebase」)。
  6. 在本程式碼實驗室中,您不需要 Google Analytics,因此請關閉 Google Analytics 選項。
  7. 按一下「Create project」,等待專案佈建完成,然後點選「Continue」。

升級 Firebase 定價方案

如要使用 Firebase App Hosting 和 Cloud Storage for Firebase,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 專案中前往「專案總覽」,然後依序點選「新增應用程式」和「網頁」。
  2. 在「App nickname」(應用程式暱稱) 文字方塊中,輸入容易記住的應用程式暱稱,例如 My Next.js app。
  3. 請勿勾選「Also set up Firebase Hosting for this app」核取方塊。
  4. 依序點選「註冊應用程式」>「前往控制台」。

在 Firebase 控制台中設定 Firebase 服務

設定驗證

  1. 在 Firebase 控制台的左側面板中,展開「Build」,然後選取「Authentication」。
  2. 按一下「開始使用」。
  3. 在「Sign-in providers」欄中,依序點選「Google」>「Enable」。
  4. 在「專案的公開名稱」文字方塊中,輸入容易記住的名稱,例如 My Next.js app。
  5. 在「專案支援電子郵件」下拉式選單中,選取您的電子郵件地址。
  6. 按一下「儲存」。

設定 Cloud Firestore

  1. 在 Firebase 控制台的左側面板中,展開「Build」,然後選取「Firestore Database」。
  2. 按一下 [Create database] (建立資料庫)。
  3. 選擇「Standard 版」,然後點選「下一步」。
  4. 請勿變更「資料庫 ID」,保留 (default) 即可。
  5. 選取資料庫的位置,然後點選「下一步」。
    如果是實際應用程式,建議選擇靠近使用者的位置。
  6. 按一下「Start in test mode」(以測試模式啟動)。請詳閱安全性規則免責事項。
    在本程式碼實驗室的後續步驟中,您將新增安全性規則來保護資料。請勿在未新增資料庫安全性規則的情況下,公開發布或公開應用程式。
  7. 點選「建立」。

設定 Cloud Storage for Firebase

  1. 在 Firebase 控制台的左側面板中,展開「Build」,然後選取「Storage」。
  2. 按一下「開始使用」。
  3. 選取預設 Storage bucket 的位置。
    US-WEST1、US-CENTRAL1 和 US-EAST1 中的 bucket 可享有 Google Cloud Storage 的「一律免費」級別。所有其他位置的值區則遵循 Google Cloud Storage 定價和用量。
  4. 按一下「Start in test mode」(以測試模式啟動)。請詳閱安全性規則免責事項。
    在本程式碼實驗室的後續步驟中,您將新增安全性規則來保護資料。請勿在未新增 Storage bucket 安全性規則的情況下,公開發布或公開應用程式。
  5. 點選「建立」。

部署安全性規則

程式碼已包含 Firestore 和 Cloud Storage for Firebase 的安全性規則。部署安全性規則後,資料庫和值區中的資料就能更妥善地受到保護,避免遭到濫用。

  1. 在終端機中,將 CLI 設為使用先前建立的 Firebase 專案:
    firebase use --add
    
    系統提示輸入別名時,請輸入 friendlyeats-codelab。
  2. 如要部署這些安全性規則 (以及稍後需要的索引),請在終端機中執行下列指令:
    firebase deploy --only firestore,storage
    
  3. 如果系統詢問:"Cloud Storage for Firebase needs an IAM Role to use cross-service rules. Grant the new role?",請按下 Enter 選取「Yes」。

4. 查看範例程式碼集

在本節中,您將查看應用程式範例程式碼集中的幾個部分,並在本程式碼實驗室中為這些部分新增功能。

資料夾和檔案結構

下表概略說明應用程式的資料夾和檔案結構:

資料夾和檔案

說明

src/components

篩選器、標頭、餐廳詳細資料和評論的 React 元件

src/lib

不一定繫結至 React 或 Next.js 的公用函式

src/lib/firebase

Firebase 專屬程式碼和 Firebase 設定

public

網頁應用程式中的靜態資產,例如圖示

src/app

使用 Next.js App Router 轉送

package.json 和 package-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 分支版本時,該後端就會自動重新建構並推出新版應用程式。

建立後端

  1. 前往 Firebase 控制台的「App Hosting」頁面。
  2. 點選「Get started」(開始使用),啟動後端建立流程。
  3. 選擇您要搜尋的區域。如果是實際應用程式,請選擇最靠近使用者的區域。
  4. 按照「Import a GitHub repository」(匯入 GitHub 存放區) 步驟中的提示,設定 GitHub 驗證。
  5. 在「存放區」中,選取「授予 GitHub 新存放區的存取權」,然後按照提示啟用您先前建立的 GitHub 存放區存取權。
  6. 按一下「Refresh list」(重新整理清單) 重新整理清單,然後選取存放區並點選「Next」(下一步)。
  7. 設定部署作業:
    1. 將使用中的分支版本設為 main。
    2. 將根目錄保留為 /。
    3. 啟用自動推出功能。
  8. 將後端命名為 friendlyeats-codelab,然後點選「Next」(下一步)。
  9. 在「連結 Firebase 網頁應用程式」部分,選擇「選取現有的 Firebase 網頁應用程式」,然後從清單中選取您新增的應用程式
  10. 按一下「完成並部署」。系統會將您重新導向至新頁面,顯示新 App Hosting 後端的狀態!
  11. 按一下「查看」,即可查看 App Hosting 部署作業的詳細資訊,包括推出狀態、記錄和用量詳細資料。
  12. 推出作業完成後,點選「網域」下方的網站網址,由於 DNS 傳播,這項作業可能需要幾分鐘才會開始。
  13. 糟糕!載入頁面時,您會看到「Application error: a 伺服器端 exception has occurred (see the 伺服器記錄 for more information)」錯誤訊息。
  14. 在 Firebase 控制台中,查看 App Hosting 後端的「Logs」(記錄) 分頁。您會看到「Error: not implemented」記錄。我們會在下一個步驟新增驗證機制,修正這個問題。

您已部署初始網頁應用程式!每當您將新的修訂版本推送至 GitHub 存放區的 main 分支版本時,Firebase 控制台就會開始新的建構和推出作業,並在推出作業完成後自動更新網站。

6. 為網頁應用程式新增驗證機制

在本節中,您將為網頁應用程式新增驗證機制,以便登入。

新增授權網域

Firebase 驗證只會接受來自允許網域的登入要求。接著,將 App Hosting 後端的網域,新增至專案的核准網域清單。

  1. 開啟「App Hosting」頁面,然後點選已部署網站下方的「查看」,前往「總覽」頁面。複製 App Hosting 後端的網域名稱。
  2. 前往「驗證設定」分頁,選擇要新增授權網域的專案。接著,找到並點選「授權網域」部分。
  3. 點按「新增網域」按鈕。
  4. 輸入 App Hosting 後端的網域。
  5. 按一下 [新增]。

實作登入和登出函式

在 src/lib/firebase/auth.js 檔案中,將 onAuthStateChanged、onIdTokenChanged、signInWithGoogle 和 signOut 函式替換為下列程式碼:

export function onAuthStateChanged(cb) {
  return _onAuthStateChanged(auth, cb);
}

export function onIdTokenChanged(cb) {
  return _onIdTokenChanged(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

說明

auth.onAuthStateChanged

新增觀察器,監控使用者登入狀態的變化。

auth.onIdTokenChanged

新增觀察器,監控使用者 ID 權杖的變更。

GoogleAuthProvider

建立 Google 驗證提供者執行個體。

signInWithPopup

啟動對話方塊式驗證流程。

auth.signOut

將使用者登出。

在 src/components/Header.jsx 檔案中,程式碼已叫用 signInWithGoogle 和 signOut 函式。

將驗證狀態傳送至伺服器

為了將驗證狀態傳遞至伺服器,我們會使用 Cookie。每當用戶端的驗證狀態變更時,我們就會更新 __session Cookie。

在 src/components/Header.jsx 中,請使用以下程式碼取代 useUserSession 函式:

function useUserSession(initialUser) {
  useEffect(() => {
    return onIdTokenChanged(async (user) => {
      if (user) {
        const idToken = await user.getIdToken();
        await setCookie("__session", idToken);
      } else {
        await deleteCookie("__session");
      }
      if (initialUser?.uid === user?.uid) {
        return;
      }
      window.location.reload();
    });
  }, [initialUser]);

  return initialUser;
}

讀取伺服器上的驗證狀態

我們將使用 FirebaseServerApp,在伺服器上鏡像用戶端的驗證狀態。

開啟 src/lib/firebase/serverApp.js,並替換 getAuthenticatedAppForUser 函式:

export async function getAuthenticatedAppForUser() {
  const authIdToken = (await cookies()).get("__session")?.value;

  // Firebase Server App is a new feature in the JS SDK that allows you to
  // instantiate the SDK with credentials retrieved from the client & has
  // other affordances for use in server environments.
  const firebaseServerApp = initializeServerApp(
    // https://github.com/firebase/firebase-js-sdk/issues/8863#issuecomment-2751401913
    initializeApp(),
    {
      authIdToken,
    }
  );

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

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

確認變更

src/app/layout.js 檔案中的根層級版面配置會轉譯標頭,並將使用者 (如有) 做為屬性傳入。

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

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

現在可以推出新版本,並驗證建構內容。

  1. 建立修訂版本,並附上修訂訊息「Add authentication」,然後推送至 GitHub 存放區。
    git add .
    
    git commit -m "Add authentication"
    
    git push
    
  2. 開啟 App Hosting 頁面,在新推出作業完成後,點選網站網址開啟網站。
  3. 測試驗證:
    1. 使用 Google 帳戶登入,並確認登入後標頭中顯示您的顯示名稱。
    2. 先登出再登入一次。您可以對不同使用者重複這個步驟。
    3. 選用:在網頁應用程式上按一下滑鼠右鍵,選取「檢視網頁原始碼」,然後搜尋顯示名稱。出現在伺服器傳回的原始 HTML 原始碼中。

7. 查看餐廳資訊

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

新增一或多間餐廳

如要將模擬餐廳資料插入本機 Cloud Firestore 資料庫,請按照下列步驟操作:

  1. 登入網頁應用程式 (如果尚未登入)。然後依序選取「2cf67d488d8e6332.png」>「新增餐廳範例」。請注意,Friendly Eats 網頁應用程式中不會顯示任何餐廳,因為我們尚未設定資料擷取程式碼。我們會在下一個步驟修正這個問題。
  2. 在 Firebase 控制台的「Firestore Database」頁面,選取「restaurants」。您會看到餐廳集合中的頂層文件,每份文件代表一間餐廳。
  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 list of restaurants from Firestore」,然後推送至 GitHub 存放區。
    git add .
    
    git commit -m "Read the list of restaurants from Firestore"
    
    git push
    
  2. 在 Firebase 控制台中開啟 App Hosting 頁面,等待新的推出作業完成。
  3. 在網頁應用程式中重新整理頁面。餐廳圖片會以圖塊形式顯示在頁面上。

確認餐廳資訊會在伺服器執行階段載入

使用 Next.js 架構時,可能無法明顯看出資料是在伺服器執行階段或用戶端執行階段載入。

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

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

在開發人員工具中停用 JavaScript

  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(() => {
    return getRestaurantsSnapshot((data) => {
      setRestaurants(data);
    }, filters);
  }, [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);

  return 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);
  });
}
  1. 在 src/lib/firebase/firestore.js 檔案中,將 getRestaurantSnapshotById() 函式替換為下列程式碼:
export function getRestaurantSnapshotById(restaurantId, cb) {
  if (!restaurantId) {
    console.log("Error: Invalid ID received: ", restaurantId);
    return;
  }

  if (typeof cb !== "function") {
    console.log("Error: The callback parameter is not a function");
    return;
  }

  const docRef = doc(db, "restaurants", restaurantId);
  return onSnapshot(docRef, (docSnap) => {
    cb({
      ...docSnap.data(),
      timestamp: docSnap.data().timestamp.toDate(),
    });
  });
}

透過「Firestore 資料庫」頁面所做的變更,現在會即時反映在網頁應用程式中。

  1. 建立修訂版本,並附上修訂訊息「Listen for realtime restaurant updates」,然後推送至 GitHub 存放區。
    git add .
    
    git commit -m "Listen for realtime restaurant updates"
    
    git push
    
  2. 在 Firebase 控制台中開啟 App Hosting 頁面,等待新的推出作業完成。
  3. 在網頁應用程式中,依序選取「27ca5d1e8ed8adfe.png」>「Add sample restaurants」。如果快照函式實作正確,餐廳就會即時顯示,不需重新整理頁面。

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}
  onSubmit={() => {
    handleClose();
  }}
>

action 屬性值是指您在下一個步驟中實作的函式。

  1. 在 src/app/actions.js 檔案中,將 handleReviewFormSubmission() 函式替換為下列程式碼:
export async function handleReviewFormSubmission(data) {
  const { firebaseServerApp } = await getAuthenticatedAppForUser();
  const db = getFirestore(firebaseServerApp);

  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. 建立修訂版本,並附上修訂訊息「Allow users to submit restaurant reviews」,然後推送至 GitHub 存放區。
    git add .
    
    git commit -m "Allow users to submit restaurant reviews"
    
    git push
    
  2. 在 Firebase 控制台中開啟 App Hosting 頁面,等待新的推出作業完成。
  3. 重新整理網頁應用程式,然後從首頁選取一間餐廳。
  4. 在餐廳頁面上,按一下 3e19beef78bb0d0e.png。
  5. 選取星級評等。
  6. 撰寫評論。
  7. 按一下「提交」。你的評論會顯示在評論清單頂端。
  8. 在 Cloud Firestore 的「新增文件」面板中,搜尋並選取您評論的餐廳文件。
  9. 在「Start collection」(開始收集) 面板中,選取「ratings」。
  10. 在「Add document」(新增文件) 面板中,找到要審查的文件,確認文件已正確插入。

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);
  setRestaurantDetails({ ...restaurantDetails, 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. 建立修訂版本,並附上修訂訊息「Allow users to change each restaurants' photo」,然後推送至 GitHub 存放區。
    git add .
    
    git commit -m "Allow users to change each restaurants' photo"
    
    git push
    
  2. 在 Firebase 控制台中開啟 App Hosting 頁面,等待新的推出作業完成。
  3. 在網頁應用程式中,確認您已登入並選取餐廳。
  4. 按一下 7067eb41fea41ff0.png,然後從檔案系統上傳圖片。圖片會離開本機環境,並上傳至 Cloud Storage。上傳後,圖片會立即顯示。
  5. 前往 Cloud Storage for Firebase。
  6. 前往代表餐廳的資料夾。您上傳的圖片已存在於資料夾中。

6cf3f9e2303c931c.png

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

在本節中,您將新增評論摘要功能,讓使用者不必逐一閱讀每則評論,就能快速瞭解大家對餐廳的看法。

將 Gemini API 金鑰儲存在 Cloud Secret Manager

  1. 須有 API 金鑰,才能使用 Gemini API。前往 Google AI Studio,然後點選「建立 API 金鑰」。
  2. 您可以視需要任意命名金鑰。如果「Choose an imported project」(選擇匯入的專案) 下方未列出您的專案,請按一下「Import project」(匯入專案),在清單中勾選您的專案,然後按一下「Import」(匯入)。最後,在「Choose an imported project」下方選取該專案,然後按一下「Create a key」。
  3. App Hosting 與 Cloud Secret Manager 整合,可讓您安全儲存 API 金鑰等機密值:
    1. 在終端機執行下列指令,建立新的 Secret:
    firebase apphosting:secrets:set GEMINI_API_KEY
    
    1. 系統提示輸入密鑰值時,請從 Google AI Studio 複製並貼上 Gemini API 金鑰。
    2. 當系統詢問新密鑰是用於正式版還是本機測試時,請選擇「正式版」。
    3. 系統詢問是否要授予存取權,讓後端服務帳戶存取密鑰時,請選取「是」。
    4. 系統詢問是否要將新的密鑰新增至 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 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 {
        if (!process.env.GEMINI_API_KEY) {
          // Make sure GEMINI_API_KEY environment variable is set:
          // https://genkit.dev/docs/get-started/
          throw new Error(
            'GEMINI_API_KEY not set. Set it with "firebase apphosting:secrets:set GEMINI_API_KEY"'
          );
        }
    
        // Configure a Genkit instance.
        const ai = genkit({
          plugins: [googleAI()],
          model: gemini20Flash, // set default model
        });
        const { text } = await ai.generate(prompt);
    
        return (
          <div className="restaurant__review_summary">
            <p>{text}</p>
            <p>✨ Summarized with Gemini</p>
          </div>
        );
      } catch (e) {
        console.error(e);
        return <p>Error summarizing reviews.</p>;
      }
    }
    
  2. 建立修訂版本,並附上修訂訊息「Use AI to summarize reviews」,然後推送至 GitHub 存放區。
    git add .
    
    git commit -m "Use AI to summarize reviews"
    
    git push
    
  3. 在 Firebase 控制台中開啟 App Hosting 頁面,等待新的推出作業完成。
  4. 開啟餐廳頁面。頁面頂端會顯示一句話,總結頁面上的所有評論。
  5. 新增評論並重新整理頁面。摘要應該會隨之變更。

11. 取消發布 App Hosting 網站

完成本程式碼實驗室後,如果不再使用應用程式,可以取消發布,確保沒有人能存取您的 Firestore、Storage 和 Gemini 資源。你隨時可以重新發布。

如要取消發布 App Hosting 網站,請按照下列步驟操作:

  1. 在 Firebase 控制台中開啟 App Hosting。
  2. 找出應用程式的後端,然後按一下「View」。
  3. 在「後端資訊」部分,按一下「網域」旁的「管理」。系統會載入「Domains」頁面。
  4. 按一下網域旁邊的「更多」圖示 (三個垂直排列的圓點),選擇「停用網域」,然後按一下「停用」確認。

12. 結論

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

瞭解詳情