Next.js 全栈框架
大约 9 分钟约 2734 字
Next.js 全栈框架
简介
Next.js 是 React 的全栈框架,提供 SSR、SSG、API 路由和文件系统路由等开箱即用能力。从 v13 的 App Router 到 v14 的 Server Actions,Next.js 不断简化全栈 React 开发。
特点
App Router
文件系统路由
app/
├── layout.tsx # 根布局
├── page.tsx # 首页 /
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/:slug
├── dashboard/
│ ├── layout.tsx # dashboard 布局
│ ├── page.tsx # /dashboard
│ └── settings/
│ └── page.tsx # /dashboard/settings
└── api/
└── users/
└── route.ts # API 端点Server Components(默认)
// app/page.tsx(默认为服务端组件)
async function HomePage() {
// 直接访问数据库(无需 API)
const products = await db.product.findMany({ take: 10 })
return (
<main>
<h1>产品列表</h1>
<ul>
{products.map(p => (
<li key={p.id}>{p.name} - ¥{p.price}</li>
))}
</ul>
</main>
)
}Client Components
'use client' // 声明为客户端组件
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>计数: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
)
}Server Actions
表单处理
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createProduct(formData: FormData) {
const name = formData.get('name') as string
const price = Number(formData.get('price'))
await db.product.create({ data: { name, price } })
revalidatePath('/products') // 刷新缓存
}
export async function deleteProduct(id: number) {
await db.product.delete({ where: { id } })
revalidatePath('/products')
}// app/products/page.tsx
import { createProduct, deleteProduct } from './actions'
async function ProductsPage() {
const products = await db.product.findMany()
return (
<div>
<h1>产品管理</h1>
{/* 表单使用 Server Action */}
<form action={createProduct}>
<input name="name" placeholder="产品名称" required />
<input name="price" type="number" step="0.01" required />
<button type="submit">添加</button>
</form>
<ul>
{products.map(p => (
<li key={p.id}>
{p.name} - ¥{p.price}
<form action={() => deleteProduct(p.id)}>
<button type="submit">删除</button>
</form>
</li>
))}
</ul>
</div>
)
}数据获取
多种渲染策略
// 1. SSR — 每次请求渲染
async function SSRPage() {
const data = await fetch('https://api.example.com/data', {
cache: 'no-store' // 不缓存
})
return <div>{/* ... */}</div>
}
// 2. SSG — 构建时生成
async function SSGPage() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // 1小时后重新验证(ISR)
})
return <div>{/* ... */}</div>
}
// 3. 静态生成 + 动态参数
export async function generateStaticParams() {
const posts = await db.post.findMany({ select: { slug: true } })
return posts.map(p => ({ slug: p.slug }))
}
async function BlogPost({ params }: { params: { slug: string } }) {
const post = await db.post.findUnique({
where: { slug: params.slug }
})
return <article>{/* ... */}</article>
}API Routes
路由处理器
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const page = Number(searchParams.get('page')) || 1
const users = await db.user.findMany({
skip: (page - 1) * 20,
take: 20
})
return NextResponse.json({ items: users, page })
}
export async function POST(request: NextRequest) {
const body = await request.json()
const user = await db.user.create({ data: body })
return NextResponse.json(user, { status: 201 })
}// app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await db.user.findUnique({
where: { id: Number(params.id) }
})
if (!user) {
return NextResponse.json({ error: '未找到' }, { status: 404 })
}
return NextResponse.json(user)
}布局和元数据
布局嵌套
// app/layout.tsx(根布局)
export const metadata = {
title: 'My App',
description: '基于 Next.js 的应用'
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>
<nav>
<Link href="/">首页</Link>
<Link href="/blog">博客</Link>
<Link href="/dashboard">管理</Link>
</nav>
{children}
</body>
</html>
)
}// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<aside className="w-60">
<nav>
<Link href="/dashboard">概览</Link>
<Link href="/dashboard/settings">设置</Link>
</nav>
</aside>
<main className="flex-1">{children}</main>
</div>
)
}渲染模式对比
Server Components 进阶
// Server Components 可以做的事情:
// 1. 直接访问数据库
// 2. 读取文件系统
// 3. 调用后端 API(不暴露给客户端)
// 4. 使用服务端密钥
// app/products/page.tsx
import { db } from '@/lib/db'
import { Suspense } from 'react'
// 并行数据获取
async function getProducts() {
return db.product.findMany({ take: 20 })
}
async function getCategories() {
return db.category.findMany()
}
// 流式渲染 — 先显示骨架屏,数据加载完再替换
function ProductListSkeleton() {
return (
<div className="space-y-4">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="h-20 bg-gray-200 animate-pulse rounded" />
))}
</div>
)
}
function ProductList() {
// 这个组件的加载不会阻塞页面其他部分
const products = db.product.findMany({ take: 20 })
return (
<ul>
{products.map(p => (
<li key={p.id}>{p.name} - ¥{p.price}</li>
))}
</ul>
)
}
async function ProductsPage() {
// 直接 await 多个请求(自动并行)
const [products, categories] = await Promise.all([
getProducts(),
getCategories(),
])
return (
<div>
<h1>产品列表</h1>
{/* Suspense 包裹异步组件 */}
<Suspense fallback={<ProductListSkeleton />}>
<ProductList />
</Suspense>
</div>
)
}Server Actions 高级用法
// app/actions/user.ts
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod' // 表单验证
// 带验证的 Server Action
const createUserSchema = z.object({
name: z.string().min(2, '名称至少 2 个字符'),
email: z.string().email('请输入有效邮箱'),
role: z.enum(['admin', 'editor', 'viewer']),
})
export async function createUser(prevState: any, formData: FormData) {
// 1. 验证输入
const result = createUserSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
role: formData.get('role'),
})
if (!result.success) {
return {
errors: result.error.flatten().fieldErrors,
message: '请修正表单中的错误',
}
}
// 2. 业务逻辑
try {
const user = await db.user.create({ data: result.data })
revalidatePath('/users') // 刷新特定路径缓存
revalidateTag('user-list') // 刷新特定标签缓存
return { success: true, user }
} catch (error) {
return { message: '创建用户失败,请重试' }
}
}
// 服务端重定向
export async function updateAndRedirect(id: number, formData: FormData) {
await db.user.update({
where: { id },
data: { name: formData.get('name') as string },
})
revalidatePath('/users')
redirect(`/users/${id}`) // 重定向
}// app/users/create/page.tsx — useActionState 处理表单
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions/user'
function CreateUserPage() {
const [state, formAction, isPending] = useActionState(createUser, {
errors: {},
message: '',
})
return (
<form action={formAction}>
{state.message && (
<p className={state.errors ? 'text-red-500' : 'text-green-500'}>
{state.message}
</p>
)}
<div>
<label>名称</label>
<input name="name" />
{state.errors?.name && (
<p className="text-red-500">{state.errors.name[0]}</p>
)}
</div>
<div>
<label>邮箱</label>
<input name="email" type="email" />
{state.errors?.email && (
<p className="text-red-500">{state.errors.email[0]}</p>
)}
</div>
<button disabled={isPending}>
{isPending ? '创建中...' : '创建用户'}
</button>
</form>
)
}认证与中间件
// middleware.ts — Next.js 中间件
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')
const { pathname } = request.nextUrl
// 保护需要认证的路由
if (pathname.startsWith('/dashboard') && !token) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(loginUrl)
}
// 已登录用户不允许访问登录页
if (pathname === '/login' && token) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return NextResponse.next()
}
// 匹配规则
export const config = {
matcher: [
'/dashboard/:path*',
'/login',
'/api/:path*',
]
}SEO 与元数据
// app/blog/[slug]/page.tsx — 动态元数据
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await db.post.findUnique({
where: { slug: params.slug },
})
return {
title: post?.title || '博客文章',
description: post?.excerpt || '',
openGraph: {
title: post?.title,
description: post?.excerpt,
images: [post?.coverImage || ''],
type: 'article',
publishedTime: post?.createdAt,
authors: [post?.authorName || ''],
},
twitter: {
card: 'summary_large_image',
title: post?.title,
description: post?.excerpt,
},
}
}
// 静态元数据
export const metadata = {
title: { default: 'My App', template: '%s | My App' },
description: '我的应用描述',
keywords: ['Next.js', 'React', '全栈'],
robots: { index: true, follow: true },
}错误处理与加载状态
// app/products/loading.tsx — 加载状态
export default function Loading() {
return (
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
<span className="ml-3">加载中...</span>
</div>
)
}
// app/products/error.tsx — 错误边界
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// 上报错误到监控平台
console.error(error)
}, [error])
return (
<div>
<h2>出了点问题</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>重试</button>
</div>
)
}
// app/products/not-found.tsx — 404 页面
export default function NotFound() {
return (
<div>
<h2>未找到</h2>
<p>请求的产品不存在</p>
<Link href="/products">返回产品列表</Link>
</div>
)
}数据缓存策略
// 1. fetch 请求缓存
// 默认缓存(SSG 行为)
const data = await fetch('https://api.example.com/data')
// 不缓存(SSR 行为)
const data = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
// 定时重新验证(ISR 行为)
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // 1 小时
})
// 2. 路由段缓存配置
// app/products/page.tsx
export const dynamic = 'force-dynamic' // 每次请求都重新渲染
export const dynamicParams = true // 允许动态参数
export const revalidate = 3600 // ISR 间隔
export const fetchCache = 'force-cache' // 强制缓存 fetch
// 3. on-demand 重新验证
// app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const body = await request.json()
if (body.type === 'tag') {
revalidateTag(body.tag)
return Response.json({ revalidated: true, now: Date.now() })
}
if (body.type === 'path') {
revalidatePath(body.path)
return Response.json({ revalidated: true, now: Date.now() })
}
}
// 4. unstable_cache — 缓存任意函数
import { unstable_cache } from 'next/cache'
const getUser = unstable_cache(
async (id: number) => db.user.findUnique({ where: { id } }),
['user'],
{ revalidate: 3600, tags: ['user-list'] }
)| 模式 | 缩写 | 渲染时机 | 适用场景 |
|---|---|---|---|
| Server Side Rendering | SSR | 每次请求 | 动态内容、SEO |
| Static Site Generation | SSG | 构建时 | 博客、文档 |
| Incremental SSR | ISR | 定期重新验证 | 内容站 |
| Client Side Rendering | CSR | 客户端 | 管理后台 |
| React Server Components | RSC | 服务端流式 | 混合渲染 |
优点
缺点
总结
Next.js 核心:App Router 文件系统路由、Server Components 默认服务端渲染、Server Actions 替代 API 调用。渲染策略:SSR(动态)、SSG(静态)、ISR(增量)。布局嵌套 layout.tsx + page.tsx。API Routes 用 route.ts 的 GET/POST 方法。推荐搭配 Prisma ORM 和 Tailwind CSS 使用。
关键知识点
- 先判断主题更偏浏览器原理、框架机制、工程化还是性能优化。
- 前端问题很多看似是页面问题,实际源头在构建、缓存、状态流或接口协作。
- 真正成熟的前端方案一定同时考虑首屏、交互、可维护性和线上诊断。
- 前端主题最好同时看浏览器原理、框架机制和工程化约束。
项目落地视角
- 把组件边界、状态归属、网络层规范和错误处理先定下来。
- 上线前检查包体积、缓存命中、接口失败路径和关键交互降级策略。
- 如果主题和性能有关,最好用 DevTools、Lighthouse 或埋点验证。
- 对关键页面先建立状态流和数据流,再考虑组件拆分。
常见误区
- 只盯框架 API,不理解浏览器和运行时成本。
- 把状态、请求和 UI 更新混成一层,后期难维护。
- 线上问题出现时没有日志、埋点和性能基线可对照。
- 只追框架新特性,不分析实际渲染成本。
进阶路线
- 继续补齐 SSR、边缘渲染、设计系统和监控告警能力。
- 把主题和后端接口约定、CI/CD、缓存策略一起思考。
- 沉淀组件规范、页面模板和性能基线,减少团队差异。
- 继续补齐设计系统、SSR/边缘渲染、监控告警和组件库治理。
适用场景
- 当你准备把《Next.js 全栈框架》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合中后台应用、门户站点、组件库和实时交互页面。
- 当需求涉及状态流、路由、网络缓存、SSR/CSR 或性能治理时,这类主题很关键。
落地建议
- 先定义组件边界和状态归属,再落地 UI 细节。
- 对核心页面做首屏、体积、缓存和错误路径检查。
- 把安全、兼容性和可访问性纳入默认交付标准。
排错清单
- 先用浏览器 DevTools 看请求、性能面板和控制台错误。
- 检查依赖版本、构建配置、环境变量和静态资源路径。
- 如果是线上问题,优先确认缓存、CDN 和构建产物是否一致。
复盘问题
- 如果把《Next.js 全栈框架》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Next.js 全栈框架》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Next.js 全栈框架》最大的收益和代价分别是什么?
