Skip to content1. 构建时
3. OAuth 登录回调后报
工程与部署
Next.js 实战应用:构建全栈博客与常见问题
Next.js 实战应用:构建全栈博客与常见问题 的概念、用法、示例和注意点
2024/01/236 分钟工程与部署
概述
本应用构建一个全栈博客,涵盖文章展示、第三方账号登录、评论提交以及管理端保护。技术栈为 Next.js(App Router)、TypeScript、Tailwind CSS、NextAuth.js、Prisma 与 SQLite。项目通过 create-next-app 初始化,在交互式选项中启用 TypeScript、App Router 与 Tailwind CSS。
主要依赖:
next-auth— 处理 OAuth 认证与 sessionprisma、@prisma/client— 数据库 ORM 与客户端sqlite3— 本地开发数据库(切换 PostgreSQL 只需修改连接字符串与 Provider)
推荐的目录组织如下:
app/
page.tsx # 首页
layout.tsx # 根布局
posts/[slug]/page.tsx # 文章详情
admin/page.tsx # 受保护的管理页
api/auth/[...nextauth]/ # NextAuth 路由处理
prisma/
schema.prisma # 数据模型
components/
CommentForm.tsx # 评论提交表单
CommentList.tsx # 评论列表
lib/
prisma.ts # PrismaClient 单例
actions/
comment.ts # 评论相关 Server Action
middleware.ts数据流为:首页列出文章,点击进入详情页,详情页底部展示评论且可提交评论。只有通过认证的用户才能发表评论,管理页面仅允许登录用户访问,由中间件在请求层面拦截未认证请求。
数据模型与 Prisma
模型定义
Prisma Schema 定义三张表:User、Post、Comment。User 存储账号信息,Post 存储文章标题与内容,Comment 关联 Post 与 User。遗留的 Account 模型由 NextAuth 的 PrismaAdapter 使用,应用层不直接操作。
prisma
model User {
id String @id @default(cuid())
name String?
email String? @unique
image String?
accounts Account[]
comments Comment[]
}
model Post {
id String @id @default(cuid())
title String
content String
createdAt DateTime @default(now())
comments Comment[]
}
model Comment {
id String @id @default(cuid())
text String
approved Boolean @default(false)
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
}Comment.approved 默认为 false,便于后期加入审核;若需要立即显示,可在创建时将值设为 true。
PrismaClient 初始化
为避免开发模式下因热重载产生多个数据库连接,将 PrismaClient 实例挂载到 global 对象。
ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = global as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma迁移命令
在定义 Schema 后执行:
bash
npx prisma migrate dev --name init该命令会生成迁移文件并应用到数据库。后续每次修改 Schema 后重复运行即可,Prisma 会自动生成增量迁移。
用户认证
认证流程基于 NextAuth.js 实现。NextAuth 通过 Route Handler 接管 /api/auth/[...nextauth],并提供客户端 hook(useSession)与服务端方法(getServerSession)。
OAuth Provider 配置
使用 GitHub 作为 OAuth Provider,用户信息经 PrismaAdapter 自动同步到 User 和 Account 表。
ts
// app/api/auth/[...nextauth]/route.ts
import NextAuth, { NextAuthOptions } from "next-auth"
import GithubProvider from "next-auth/providers/github"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { prisma } from "@/lib/prisma"
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
],
callbacks: {
session({ session, user }) {
if (session.user) {
session.user.id = user.id
}
return session
},
},
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }session 回调将数据库 user.id 注入 session.user 对象,以便在数据操作中关联用户记录。
客户端登录/登出
在客户端组件中使用 useSession 获取当前会话,并调用 signIn / signOut。
tsx
'use client'
import { signIn, signOut, useSession } from 'next-auth/react'
export default function AuthButton() {
const { data: session } = useSession()
if (session) {
return (
<div className="flex items-center gap-2">
<span>{session.user?.name}</span>
<button onClick={() => signOut()}>登出</button>
</div>
)
}
return <button onClick={() => signIn('github')}>登录</button>
}受保护路由
通过中间件统一拦截未认证请求,避免在每个页面中单独检查会话。在项目根目录创建 middleware.ts:
ts
import { withAuth } from "next-auth/middleware"
import { NextResponse } from "next/server"
export default withAuth(
function middleware(req) {
return NextResponse.next()
},
{
callbacks: {
authorized({ token }) {
return !!token
},
},
},
)
export const config = { matcher: ["/admin/:path*"] }withAuth 由 NextAuth 提供,它检查请求中的 token 是否存在。matcher 限定仅匹配 /admin 路径下的所有子路由。未登录用户访问时会被重定向至 NextAuth 默认的登录页。更细粒度的权限控制(例如角色校验)可在回调中注入自定义字段并在组件或 Server Action 中校验。
评论功能
提交评论(Server Action)
评论提交需经过服务端校验并写入数据库,同时使相关页面缓存失效。定义 Server Action 文件 actions/comment.ts:
ts
'use server'
import { prisma } from "@/lib/prisma"
import { getServerSession } from "next-auth"
import { authOptions } from "@/app/api/auth/[...nextauth]/route"
import { revalidatePath } from "next/cache"
export async function submitComment(formData: FormData) {
const session = await getServerSession(authOptions)
if (!session?.user?.id) {
throw new Error("未登录")
}
const text = formData.get("text") as string
const postId = formData.get("postId") as string
if (!text || !postId) return
await prisma.comment.create({
data: {
text,
postId,
userId: session.user.id,
},
})
revalidatePath(`/posts/${postId}`)
}revalidatePath 清除 Next.js 对该路径的客户端路由缓存,使下次访问时重新渲染服务端组件以包含新评论。对于静态生成的页面,还需要配合 dynamic = 'force-dynamic' 或重新验证标签,避免一直返回静态内容。
组件拆分
CommentForm 为客户端组件,使用 action 属性指向 Server Action,浏览器在提交时由 Next.js 接管请求,无需手动阻止默认行为。
tsx
'use client'
import { useSession } from "next-auth/react"
import { submitComment } from "@/actions/comment"
export default function CommentForm({ postId }: { postId: string }) {
const { data: session } = useSession()
if (!session) return <p>请登录后发表评论</p>
return (
<form action={submitComment} className="space-y-2 mb-4">
<input type="hidden" name="postId" value={postId} />
<textarea name="text" required rows={3} className="border w-full p-1" />
<button type="submit" className="px-3 py-1 bg-black text-white">提交</button>
</form>
)
}CommentList 作为服务端组件直接从数据库读取已审核通过的评论:
tsx
import { prisma } from "@/lib/prisma"
export async function CommentList({ postId }: { postId: string }) {
const comments = await prisma.comment.findMany({
where: { postId, approved: true },
include: { user: { select: { name: true, image: true } } },
orderBy: { createdAt: "desc" },
})
return (
<ul className="space-y-3">
{comments.map((c) => (
<li key={c.id} className="border p-2 rounded">
<p className="text-sm text-gray-600">{c.user.name}</p>
<p>{c.text}</p>
</li>
))}
</ul>
)
}在文章详情页中组合这些组件。以下示例通过路径参数 params.slug 匹配文章 ID(假设 ID 可直接用作 slug,实际项目可另行定义 slug 字段):
tsx
// app/posts/[slug]/page.tsx
import { prisma } from "@/lib/prisma"
import { CommentList } from "@/components/CommentList"
import CommentForm from "@/components/CommentForm"
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await prisma.post.findUnique({ where: { id: params.slug } })
if (!post) return <div>文章不存在</div>
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
<hr />
<CommentForm postId={post.id} />
<CommentList postId={post.id} />
</article>
)
}SEO 与元数据
Next.js 的 Metadata API 允许在 layout.tsx 或 page.tsx 中导出 metadata 对象或 generateMetadata 函数,框架会自动将其注入 <head>。
根布局可提供整站默认值,各页面通过 template 拼接标题:
tsx
// app/layout.tsx
import type { Metadata } from "next"
export const metadata: Metadata = {
title: {
default: "我的博客",
template: "%s | 我的博客",
},
description: "记录前端、Next.js 和 Web 开发",
}文章详情页使用 generateMetadata 动态获取标题与描述,并生成 Open Graph 标签以便社交分享。
tsx
import { Metadata } from "next"
interface Props {
params: { slug: string }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await prisma.post.findUnique({
where: { id: params.slug },
select: { title: true, content: true },
})
if (!post) return { title: "文章不存在" }
return {
title: post.title,
description: post.content.slice(0, 160),
openGraph: {
title: post.title,
description: post.content.slice(0, 160),
type: "article",
},
}
}部署与环境变量
项目可部署至 Vercel 或任意支持 Node.js 的服务。Vercel 会检测 Next.js 项目并自动执行 next build。若自行部署,需在 package.json 中设置构建与启动脚本:
json
{
"scripts": {
"build": "next build",
"start": "next start"
}
}然后通过 npm run build && npm run start 启动,通常配合进程管理器(如 pm2 或 systemd)保持运行。
关键环境变量:
DATABASE_URL— Prisma 数据库连接字符串(如file:./dev.db或 PostgreSQL URL)GITHUB_ID、GITHUB_SECRET— GitHub OAuth 应用凭证NEXTAUTH_URL— 站点完整地址(Vercel 部署时可自动推断自VERCEL_URL)NEXTAUTH_SECRET— 用于加密 session 与 token,务必设置一个随机字符串
带有 NEXT_PUBLIC_ 前缀的变量会暴露给浏览器,凭证类变量不可使用此前缀。
首次部署或数据库结构变更后,需要执行迁移命令而非生成新迁移:
bash
npx prisma migrate deploy该命令只应用已存在的迁移文件,适合 CI/CD 环境。
性能优化
图片优化
使用 next/image 替代原生 <img>。该组件自动生成响应式尺寸,支持格式转换(如 WebP)与懒加载。
tsx
import Image from "next/image"
<Image
src="/avatar.jpg"
width={80}
height={80}
alt="头像"
className="rounded-full"
/>引用外部图片时需在 next.config.js 中配置允许的域名:
js
module.exports = {
images: {
domains: ['avatars.githubusercontent.com'],
},
}否则构建或运行时会提示图片域名未授权。
字体优化
next/font 加载 Google Fonts 时,Next.js 会生成自托管字体文件,避免外部 CDN 阻塞渲染,并消除布局偏移。在根布局中引入:
tsx
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({ children }) {
return (
<html lang="zh" className={inter.className}>
<body>{children}</body>
</html>
)
}动态导入
评论相关组件可延迟加载,减小首屏 JavaScript 体积。用 next/dynamic 包装 CommentList:
tsx
import dynamic from 'next/dynamic'
const CommentList = dynamic(() => import('@/components/CommentList'), {
loading: () => <p>加载评论中…</p>,
ssr: false,
})设置 ssr: false 时,该组件完全在客户端渲染,适合 SEO 无影响的交互区域。若评论需要被搜索引擎索引,则保留服务端渲染。
常见问题排查
1. 构建时 next build 报错找不到 fs 模块
该错误通常源于客户端组件或带有 'use client' 指令的文件中引入了仅服务端可用的模块(如直接 import { prisma } from …)。Prisma Client 依赖 Node.js 原生模块,无法在浏览器环境执行。所有数据库操作必须限定在 Server Actions、Route Handler 或 getServerSideProps(Pages Router)中。检查所有客户端组件,确保没有引入 Prisma 或类似服务端包。
2. 评论提交后页面不显示新评论
首先确认服务端是否正常写入了数据库。若已写入,问题多集中在缓存刷新。检查 revalidatePath 是否正确指向当前路径,且页面配置未设为静态生成而未启用动态重验证。如果页面通过 fetch 缓存了数据,且未使用 revalidateTag 关联,则 revalidatePath 可能不会刷新内层缓存。此时可将 revalidatePath 替换为 revalidateTag,并在数据获取时通过 next: { tags: [...] } 标记。
ts
// 提交时
revalidateTag(`post-${postId}`)
// 查询时
await fetch(`...`, { next: { tags: [`post-${postId}`] } })3. OAuth 登录回调后报 Invalid token
多见于 NEXTAUTH_SECRET 缺失或值不匹配。开发环境下 NextAuth 会生成临时密钥,部署到生产后若不显式设置,密钥便不存在或每次构建不同,导致已有 session 失效。可通过 openssl rand -base64 32 生成长密钥,并固定写入环境变量。同时确保 NEXTAUTH_URL 与实际访问地址一致,否则回调验证可能失败。
参考链接
[1] Next.js 项目结构约定:https://nextjs.org/docs/app/getting-started/project-structure
[2] Prisma Schema 定义:https://www.prisma.io/docs/orm/prisma-schema/overview
[3] Prisma 迁移命令:https://www.prisma.io/docs/orm/prisma/migrate/getting-started
[4] Next.js 中间件:https://nextjs.org/docs/app/building-your-application/routing/middleware
[5] Next.js 认证方案:https://nextjs.org/docs/app/building-your-application/authentication
[6] Server Actions 与数据变更:https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
[7] 缓存重新验证:https://nextjs.org/docs/app/building-your-application/data-fetching/revalidating
[8] 环境变量配置:https://nextjs.org/docs/app/building-your-application/configuring/environment-variables
[9] next/image 图片优化:https://nextjs.org/docs/app/building-your-application/optimizing/images
[10] next/font 字体优化:https://nextjs.org/docs/app/building-your-application/optimizing/fonts
[11] 动态导入与懒加载:https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading
[12] 部署指南:https://nextjs.org/docs/app/building-your-application/deploying
[13] 错误处理机制:https://nextjs.org/docs/app/building-your-application/routing/error-handling
[14] Next.js 官方文档:https://nextjs.org/docs
参考链接
- [1] https://nextjs.org/docs/app/getting-started/project-structure
- [2] https://www.prisma.io/docs/orm/prisma-schema/overview
- [3] https://www.prisma.io/docs/orm/prisma/migrate/getting-started
- [4] https://nextjs.org/docs/app/building-your-application/routing/middleware
- [5] https://nextjs.org/docs/app/building-your-application/authentication
- [6] https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
- [7] https://nextjs.org/docs/app/building-your-application/data-fetching/revalidating
- [8] https://nextjs.org/docs/app/building-your-application/configuring/environment-variables
- [9] https://nextjs.org/docs/app/building-your-application/optimizing/images
- [10] https://nextjs.org/docs/app/building-your-application/optimizing/fonts
- [11] https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading
- [12] https://nextjs.org/docs/app/building-your-application/deploying
- [13] https://nextjs.org/docs/app/building-your-application/routing/error-handling
- [14] https://nextjs.org/docs
相关推荐
2024/01/21 · 10 分钟Next.js 进阶能力:流式渲染、并行路由与拦截路由
Next.js 进阶能力:流式渲染、并行路由与拦截路由 的概念、用法、示例和注意点
2024/01/19 · 5 分钟Next.js 关键 API:路由处理程序、中间件与 Server ActionsNext.js 关键 API:路由处理程序、中间件与 Server Actions 的概念、用法、示例和注意点
2025/03/09 · 7 分钟Nuxt.js 渲染模式与部署Nuxt.js 渲染模式与部署 的概念、用法、示例和注意点
2025/03/03 · 8 分钟Nuxt.js 核心组件与 APINuxt.js 核心组件与 API 的概念、用法、示例和注意点
