首页
/ Next.js 集成 Firebase 指南:基于 with-firebase 示例的客户端/服务端双端架构实战

Next.js 集成 Firebase 指南:基于 with-firebase 示例的客户端/服务端双端架构实战

2026-09-06 19:10:08作者:姚月梅Lane

导读

本篇文章以 Next.js 官方仓库 examples/with-firebase 示例为蓝本,系统讲解如何在 Next.js 应用中接入 Firebase(客户端 SDK)与 firebase-admin(服务端 SDK)。文章覆盖项目的目录结构与初始化流程、浏览器端与 Node.js 服务端"双 Firebase 实例"的架构设计、React Context 驱动的用户状态管理、利用 Cloud Firestore 在 getServerSideProps 中做服务端数据读取,以及本地 .env.local 配置与 Vercel 部署的完整要点。读完本文,你将掌握一套可复制、可在生产环境演进的前后端一体 Firebase 接入方案。

示例概览:一个前后端联动的 Firebase 最小应用

examples/with-firebase 是 Next.js 官方仓库中的一个示例应用,其定位是"为客户端应用准备的 Firebase 简单集成"(README 原文:This is a simple set up for Firebase for client side applications)。它的巧妙之处在于同时演示了 Firebase 的两种使用形态:

  • 浏览器端(客户端):使用 Firebase JavaScript SDK(firebase 9.x 模块化 API)完成初始化、Analytics 埋点与 Auth 登录状态监听,并将用户状态通过 React Context 提供给全应用。
  • Node.js 服务端(SSR):使用 firebase-admin 通过服务账号私钥初始化,进而在 Next.js 的 getServerSideProps 中直连 Firestore 读取数据,实现服务端渲染。

该应用的完整目录结构如下(见 examples/with-firebase):

examples/with-firebase/
├── context/
│   └── userContext.js        # React Context:承载 Firebase Auth 用户状态
├── fetchData/
│   └── getProfileData.js     # 服务端取数:用 firebase-admin 读 Firestore
├── firebase/
│   ├── clientApp.js          # 浏览器端 Firebase 初始化(模块化 SDK)
│   └── nodeApp.js            # 服务端 firebase-admin 初始化
├── pages/
│   ├── _app.js               # 用 UserProvider 包裹全应用
│   ├── index.js              # 首页:客户端写入 Firestore 演示
│   └── profile/
│       └── [username].js     # SSR 页:getServerSideProps 服务端读库
├── public/
│   └── favicon.ico
├── .env.local.example        # 环境变量模板
├── package.json
└── vercel.json

package.json(见 examples/with-firebase/package.json)可以看到技术栈版本基线:

  • firebase: 9.1.1(模块化/Compat 并存的新一代 SDK)
  • firebase-admin: 9.12.0
  • next: latestreact: ^18.2.0react-dom: ^18.2.0

页面之间通过一条业务链路串联:首页(pages/index.js)在浏览器端用 Firestore 写入一份 profile 文档,随后通过 next/link 跳转到 /profile/nextjs_user,该动态路由页面在服务端用 firebase-admin 读取同一份文档并渲染出来。一个示例就串起了 Firebase 在客户端写入、在服务端读取的完整闭环。

快速启动:用 create-next-app 初始化示例

与仓库内其他示例一致,with-firebase 可以通过官方脚手架一键拉取。README 提供了 npm、Yarn、pnpm 三种包管理器的等价命令(见 examples/with-firebase/README.md):

npx create-next-app --example with-firebase with-firebase-app
yarn create next-app --example with-firebase with-firebase-app
pnpm create next-app --example with-firebase with-firebase-app

脚手架会基于 examples/with-firebase 在当前目录创建名为 with-firebase-app 的新项目并自动安装依赖。之后进入项目目录即可使用 examples/with-firebase/package.json 中声明的标准脚本:

npm run dev     # 开发模式,等价于 next dev
npm run build   # 生产构建,等价于 next build
npm run start   # 启动生产服务,等价于 next start

环境变量配置:把 Firebase 凭据接入 .env.local

逐项说明:NEXT_PUBLIC_ 前缀的语义

Firebase 的 Web 应用配置(apiKey、authDomain 等)需要暴露给浏览器端代码使用。Next.js 规定:只有以 NEXT_PUBLIC_ 开头的环境变量才会被内联进客户端 bundle;而服务端专用凭据(私钥)不能带此前缀,以避免被打进浏览器产物。示例的模板文件 examples/with-firebase/.env.local.example 对两类变量做了清晰区隔:

# 来自 Firebase 控制台 "Project settings" 的 Web App 配置
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_DATABASE_URL=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=

# 用于 firebase-admin(仅服务端读取,切勿暴露给浏览器)
FIREBASE_CLIENT_EMAIL=
FIREBASE_PRIVATE_KEY=

.env.local.example 本身就是一份可执行的配置清单:文件末尾注明"# TODO. Fill in with Firebase Config",字段留空待填。

三步完成配置

  1. Firebase 控制台 创建 Firebase 项目,并在其中新增一个 Web App,从而获得一份完整的客户端配置对象。
  2. 复制模板为本地环境变量文件(该文件已被仓库 .gitignore 忽略,不会提交):
    cp .env.local.example .env.local
    
  3. 依次把控制台中的每个字段填入 .env.local 对应变量。其中 FIREBASE_MEASUREMENT_ID 对应 Analytics 的测量 ID,属于可选字段(详见下文客户端初始化对 measurementId 的守卫逻辑)。

服务端专用:Service Account 私钥的获取

若需要跑通 SSR 页面(/profile/[username]),还必须在 Firebase 控制台的 Project settings > Service accounts 中点击 Generate new private key,将服务账号凭据以 JSON 形式下载,然后取出其中的 client_emailprivate_key,分别填入 FIREBASE_CLIENT_EMAILFIREBASE_PRIVATE_KEY。README 特别强调这是"如果你想查看 SSR 页面"时的必需步骤——因为浏览器端的 apiKey 无法通过 firebase-admin 的身份校验访问 Firestore。

客户端初始化:firebase/clientApp.js

客户端 Firebase 实例的创建集中在 examples/with-firebase/firebase/clientApp.js

import { initializeApp, getApps } from "firebase/app";
import { getAnalytics } from "firebase/analytics";

export const createFirebaseApp = () => {
  const clientCredentials = {
    apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
    authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
    databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
    projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
    storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
    messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
    appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
    measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
  };

  if (getApps().length <= 0) {
    const app = initializeApp(clientCredentials);
    // Check that `window` is in scope for the analytics module!
    if (typeof window !== "undefined") {
      // Enable analytics. https://firebase.google.com/docs/analytics/get-started
      if ("measurementId" in clientCredentials) {
        getAnalytics();
      }
    }
    return app;
  }
};

这段实现有四个值得注意的工程细节:

  1. 单例防重复:通过 getApps().length <= 0 判断当前是否已存在初始化实例,避免在模块被多处引用或页面热更新时重复调用 initializeApp 抛出 "Firebase App named '[DEFAULT]' already exists" 错误。
  2. 环境变量集中映射:把 8 个 NEXT_PUBLIC_FIREBASE_* 变量一次性映射为 Firebase 客户端配置对象,字段名与 Firebase Web 配置一一对应,后续扩展字段只需改动此处。
  3. window 作用域守卫getAnalytics() 只在 typeof window !== "undefined" 时才被调用,防止在服务端渲染阶段(不存在 window)误触发 Analytics 初始化。
  4. measurementId 可选"measurementId" in clientCredentials 判断保证未配置 Analytics 时应用仍可正常运行,不会因缺字段抛错。

在 React Context 中消费客户端实例:context/userContext.js

examples/with-firebase/context/userContext.js 演示了如何把 Auth 状态接入 React Context:

import { useState, useEffect, createContext, useContext } from "react";
import { createFirebaseApp } from "../firebase/clientApp";
import { getAuth, onAuthStateChanged } from "firebase/auth";

export const UserContext = createContext();

export default function UserContextComp({ children }) {
  const [user, setUser] = useState(null);
  const [loadingUser, setLoadingUser] = useState(true); // Helpful, to update the UI accordingly.

  useEffect(() => {
    // Listen authenticated user
    const app = createFirebaseApp();
    const auth = getAuth(app);
    const unsubscriber = onAuthStateChanged(auth, async (user) => {
      try {
        if (user) {
          // User is signed in.
          const { uid, displayName, email, photoURL } = user;
          // You could also look for the user doc in your Firestore (if you have one):
          // const userDoc = await firebase.firestore().doc(`users/${uid}`).get()
          setUser({ uid, displayName, email, photoURL });
        } else setUser(null);
      } catch (error) {
        // Most probably a connection error. Handle appropriately.
      } finally {
        setLoadingUser(false);
      }
    });

    // Unsubscribe auth listener on unmount
    return () => unsubscriber();
  }, []);

  return (
    <UserContext.Provider value={{ user, setUser, loadingUser }}>
      {children}
    </UserContext.Provider>
  );
}

// Custom hook that shorthands the context!
export const useUser = () => useContext(UserContext);

它的设计要点:

  • loadingUser 状态:初始为 true,用于区分"登录态尚未就绪"与"用户确实未登录",避免界面在鉴权回调返回前闪现错误内容。README 与源码注释都点明这是"便于据此更新 UI"的关键标志位。
  • 订阅与清理配对onAuthStateChanged 返回的 unsubscriberuseEffect 清理函数中被调用,确保组件卸载时停止监听、避免内存泄漏。
  • 用户子集化存储:只把 uiddisplayNameemailphotoURL 存入 Context,避免把整个 Firebase User 对象(含内部状态)散落到全应用。注释中还预留了从 Firestore 按 uid 补充读取用户扩展文档的思路。
  • Provider 与自定义 Hook 二合一:默认导出的 Provider 组件负责装配状态,具名导出 useUser 让任意层级的组件都能通过 useContext(UserContext) 一行拿到 { user, setUser, loadingUser }

全局注入 Provider:pages/_app.js

为了让用户状态覆盖所有页面,示例在自定义 App 中包裹 Provider(见 examples/with-firebase/pages/_app.js):

import UserProvider from "../context/userContext";

// Custom App to wrap it with context provider
export default function App({ Component, pageProps }) {
  return (
    <UserProvider>
      <Component {...pageProps} />
    </UserProvider>
  );
}

这里的映射关系为:UserProvider(来自 userContext.js 的默认导出 UserContextComp)包裹每个页面的根组件,从而让首页组件可以通过 useUser() 读取用户状态。

服务端初始化:firebase/nodeApp.js 与 SSR 取数

firebase-admin 的双重防护

服务端实例位于 examples/with-firebase/firebase/nodeApp.js

import * as admin from "firebase-admin";

if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.cert({
      projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
      clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
      privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"),
    }),
    databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
  });
}

export default admin;

这里有一个极易踩坑且值得重点讲解的细节:FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n")。从 Firebase 控制台下载的私钥 JSON 中包含带字面 \n 转义序列的 PEM 私钥,而通过 .env.local 加载时这些 \n 会被原样保留为两个字符;若不替换,admin.credential.cert 解析出的将是单行"假私钥",最终导致服务端认证失败(错误信息形如 error:0909006C:PEM routines:get_name:no start line)。示例在此用正则把字面 \n 还原为真实换行,是这份配置能跑通的关键。

与客户端同理,!admin.apps.length 确保 Node 端也只有一个初始化实例,防止 Next.js 开发模式下模块被反复求值导致重复初始化报错。

需要提醒的是,admin.credential.cert 只能运行在 Node.js 服务端环境中,因此承载它的 nodeApp.js 绝不能被客户端模块 import——一旦进入浏览器 bundle,私钥与初始化代码都会泄露。示例中仅由 fetchData/getProfileData.js 引用该文件,而后者只被 getServerSideProps 调用,从而把安全边界限定在服务端。

getServerSideProps 中的 Firestore 读取

服务端取数函数位于 examples/with-firebase/fetchData/getProfileData.js

import admin from "../firebase/nodeApp";

export const getProfileData = async (username) => {
  const db = admin.firestore();
  const profileCollection = db.collection("profile");
  const profileDoc = await profileCollection.doc(username).get();

  if (!profileDoc.exists) {
    return null;
  }

  return profileDoc.data();
};

页面侧通过动态路由 [username].js 调用它(见 examples/with-firebase/pages/profile/[username].js):

export default function SSRPage({ data }) {
  const { username, profile } = data;
  // ... 渲染 username 与 profile.message
}

export const getServerSideProps = async ({ params }) => {
  const { username } = params;
  const profile = await getProfileData(username);
  if (!profile) {
    return { notFound: true };
  }
  return { props: { data: { username, profile } } };
};

该页面演示了服务端渲染(SSR)下的两种最佳实践:

  1. 把 firebase-admin 的读写全部封装在服务端getServerSideProps 在每次请求时于服务端执行 getProfileData,浏览器永远只收到渲染完成的 HTML 与结构化 props,不接触任何管理端凭据。
  2. notFound: true 兜底 404:当 getProfileData 返回 null(即 Firestore 中不存在该用户文档)时,页面返回 404 而不是渲染空内容,符合 Next.js 对 getServerSideProps 返回值的约定。

一条跨端数据链路回顾

两个页面构成完整的读写闭环:

  • 首页 examples/with-firebase/pages/index.js纯客户端写入示例——它通过 useUser() 拿到 { loadingUser, user },在 useEffect 中等待 loadingUser 变 false 后再读取 user(注释点明此时"用户要么已登录、要么已登出,状态确定");点击按钮后调用:

    const db = getFirestore();
    await setDoc(doc(db, "profile", profile.username), profile);
    

    这里 profile 是一个硬编码对象 { username: "nextjs_user", message: "Awesome!!" },写入集合 profile、文档 ID 为 username。页面同时提示:该写入行为依赖 Cloud Firestore 的安全规则放行("Cloud Firestore Security Rules write permissions are required for adding users"),即生产环境必须显式配置允许当前登录用户写 profile/ 集合的规则,而不能依赖默认的"全部拒绝"。

  • SSR 页面 /profile/nextjs_user 则是服务端读取示例,通过 Link(见 next/link 用法)从首页跳转触发。

依赖与版本基线说明

示例声明了明确的最小依赖集合(examples/with-firebase/package.json),可作为集成时的版本参照:

依赖 版本 用途
firebase 9.1.1 浏览器端模块化 SDK:firebase/appfirebase/analyticsfirebase/authfirebase/firestore
firebase-admin 9.12.0 Node.js 服务端 SDK:证书认证 + Firestore 管理
next latest React 框架本体(Pages Router 示例)
react / react-dom ^18.2.0 渲染层

需要说明的适用前提:该示例基于 Next.js Pages Routerfirebase 9 模块化 API 编写(import { initializeApp } from "firebase/app" 即为 v9 风格的具名导入,区别于 v8 的命名空间 firebase.initializeApp())。在复用到 App Router 项目时,getServerSideProps 需对应替换为 Server Component 或 Route Handler,但 clientApp.js 的单例守卫、Context 订阅清理、nodeApp.js 的私钥换行处理等核心模式均可平移。

部署到 Vercel:环境变量的关键一步

README 提供了两条部署路径(examples/with-firebase/README.md):

  1. 一键部署:点击示例页顶部的 "Deploy with Vercel" 按钮,从仓库模板直接创建 Vercel 项目并克隆源码。
  2. 本地项目部署:把项目推送到 GitHub/GitLab/Bitbucket 后,在 Vercel 中 Import 该仓库。

无论哪种方式,README 都给出了一条重要提示Important):导入项目后,务必在 Vercel 的 Environment Variables 面板中逐项配置环境变量,使其与本地 .env.local 完全一致。由于服务端页面依赖 FIREBASE_CLIENT_EMAILFIREBASE_PRIVATE_KEY 两个不含 NEXT_PUBLIC_ 前缀的私有变量,且客户端代码依赖 8 个 NEXT_PUBLIC_FIREBASE_* 变量,任何一项缺失都会导致对应端在运行时失败——这正是示例通过 .env.local.example 对变量做注释分类的原因。

小结:示例的可复用模式清单

examples/with-firebase 拆解后,可以提炼出一套在任何 Next.js + Firebase 项目中都适用的模式:

  1. 双实例隔离firebase/clientApp.js(浏览器)与 firebase/nodeApp.js(服务端 admin)各司其职,getApps()/admin.apps 长度守卫保证单例。
  2. 环境变量命名即安全边界NEXT_PUBLIC_ 前缀区分"可进浏览器"与"仅存服务端"两类凭据;服务端私钥换行 \n 必须在取值时还原。
  3. Context 承载登录态onAuthStateChanged 订阅 + unsubscriber 清理 + loadingUser 加载位 + 用户字段白名单化,构成健壮的客户端鉴权状态层。
  4. SSR 只走服务端 SDKgetServerSideProps 内用 firebase-admin 取数,缺失数据以 notFound: true 优雅兜底。
  5. 安全规则不可少:客户端直接写 Firestore 时必须配套配置 Cloud Firestore Security Rules,示例仅用于演示链路,不可照搬到生产而不加规则。

在动手实践前,建议先通读本仓库中的 examples/with-firebase/README.md 与各源码文件,再结合本文对关键实现的剖析,便能快速建立起一套前后端贯通、可在真实项目中演进的 Firebase 集成基座。

登录后查看全文
热门项目推荐
相关项目推荐