Next.js 集成 Firebase 指南:基于 with-firebase 示例的客户端/服务端双端架构实战
导读
本篇文章以 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(
firebase9.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.0next: latest、react: ^18.2.0、react-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",字段留空待填。
三步完成配置
- 在 Firebase 控制台 创建 Firebase 项目,并在其中新增一个 Web App,从而获得一份完整的客户端配置对象。
- 复制模板为本地环境变量文件(该文件已被仓库
.gitignore忽略,不会提交):cp .env.local.example .env.local - 依次把控制台中的每个字段填入
.env.local对应变量。其中FIREBASE_MEASUREMENT_ID对应 Analytics 的测量 ID,属于可选字段(详见下文客户端初始化对measurementId的守卫逻辑)。
服务端专用:Service Account 私钥的获取
若需要跑通 SSR 页面(/profile/[username]),还必须在 Firebase 控制台的 Project settings > Service accounts 中点击 Generate new private key,将服务账号凭据以 JSON 形式下载,然后取出其中的 client_email 与 private_key,分别填入 FIREBASE_CLIENT_EMAIL 与 FIREBASE_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;
}
};
这段实现有四个值得注意的工程细节:
- 单例防重复:通过
getApps().length <= 0判断当前是否已存在初始化实例,避免在模块被多处引用或页面热更新时重复调用initializeApp抛出 "Firebase App named '[DEFAULT]' already exists" 错误。 - 环境变量集中映射:把 8 个
NEXT_PUBLIC_FIREBASE_*变量一次性映射为 Firebase 客户端配置对象,字段名与 Firebase Web 配置一一对应,后续扩展字段只需改动此处。 window作用域守卫:getAnalytics()只在typeof window !== "undefined"时才被调用,防止在服务端渲染阶段(不存在 window)误触发 Analytics 初始化。- 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返回的unsubscriber在useEffect清理函数中被调用,确保组件卸载时停止监听、避免内存泄漏。 - 用户子集化存储:只把
uid、displayName、email、photoURL存入 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)下的两种最佳实践:
- 把 firebase-admin 的读写全部封装在服务端:
getServerSideProps在每次请求时于服务端执行getProfileData,浏览器永远只收到渲染完成的 HTML 与结构化 props,不接触任何管理端凭据。 - 用
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/app、firebase/analytics、firebase/auth、firebase/firestore |
firebase-admin |
9.12.0 |
Node.js 服务端 SDK:证书认证 + Firestore 管理 |
next |
latest |
React 框架本体(Pages Router 示例) |
react / react-dom |
^18.2.0 |
渲染层 |
需要说明的适用前提:该示例基于 Next.js Pages Router 与 firebase 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):
- 一键部署:点击示例页顶部的 "Deploy with Vercel" 按钮,从仓库模板直接创建 Vercel 项目并克隆源码。
- 本地项目部署:把项目推送到 GitHub/GitLab/Bitbucket 后,在 Vercel 中 Import 该仓库。
无论哪种方式,README 都给出了一条重要提示(Important):导入项目后,务必在 Vercel 的 Environment Variables 面板中逐项配置环境变量,使其与本地 .env.local 完全一致。由于服务端页面依赖 FIREBASE_CLIENT_EMAIL、FIREBASE_PRIVATE_KEY 两个不含 NEXT_PUBLIC_ 前缀的私有变量,且客户端代码依赖 8 个 NEXT_PUBLIC_FIREBASE_* 变量,任何一项缺失都会导致对应端在运行时失败——这正是示例通过 .env.local.example 对变量做注释分类的原因。
小结:示例的可复用模式清单
把 examples/with-firebase 拆解后,可以提炼出一套在任何 Next.js + Firebase 项目中都适用的模式:
- 双实例隔离:
firebase/clientApp.js(浏览器)与firebase/nodeApp.js(服务端 admin)各司其职,getApps()/admin.apps长度守卫保证单例。 - 环境变量命名即安全边界:
NEXT_PUBLIC_前缀区分"可进浏览器"与"仅存服务端"两类凭据;服务端私钥换行\n必须在取值时还原。 - Context 承载登录态:
onAuthStateChanged订阅 +unsubscriber清理 +loadingUser加载位 + 用户字段白名单化,构成健壮的客户端鉴权状态层。 - SSR 只走服务端 SDK:
getServerSideProps内用 firebase-admin 取数,缺失数据以notFound: true优雅兜底。 - 安全规则不可少:客户端直接写 Firestore 时必须配套配置 Cloud Firestore Security Rules,示例仅用于演示链路,不可照搬到生产而不加规则。
在动手实践前,建议先通读本仓库中的 examples/with-firebase/README.md 与各源码文件,再结合本文对关键实现的剖析,便能快速建立起一套前后端贯通、可在真实项目中演进的 Firebase 集成基座。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00