Next.js 进阶
大约 8 分钟约 2272 字
Next.js 进阶
Next.js 全栈框架概览
Next.js 是 React 生态中最成熟的全栈框架,提供了 SSR(服务端渲染)、SSG(静态站点生成)、ISR(增量静态再生)等多种渲染策略。App Router(Next.js 13+)引入了 React Server Components、Server Actions、Streaming 等新特性,改变了 React 应用的开发模式。
渲染策略选择
| 策略 | 适用场景 | 数据新鲜度 | 构建时间 |
|---|---|---|---|
| SSR | 动态内容、个性化页面 | 每次请求最新 | 无预构建 |
| SSG | 博客、文档、营销页 | 构建时确定 | 较长 |
| ISR | 频繁更新的内容页 | 定期刷新 | 适中 |
| CSR | 仪表盘、管理后台 | 客户端获取 | 最快 |
App Router
文件系统路由
app/
layout.tsx — 根布局(<html> 和 <body>)
page.tsx — 首页 (/)
loading.tsx — 全局加载状态
error.tsx — 全局错误处理
not-found.tsx — 404 页面
globals.css — 全局样式
dashboard/
layout.tsx — 仪表盘布局
page.tsx — 仪表盘首页 (/dashboard)
loading.tsx — 仪表盘加载状态
[id]/
page.tsx — 动态路由 (/dashboard/123)
api/
users/
route.ts — API 路由 (/api/users)
users/[id]/
route.ts — 动态 API 路由 (/api/users/123)布局系统
// app/layout.tsx — 根布局
import './globals.css';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: '我的应用',
description: 'Next.js 全栈应用',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body className={inter.className}>
{/* 全局 Provider */}
<ThemeProvider>
<AuthProvider>
<Navbar />
<main>{children}</main>
<Footer />
</AuthProvider>
</ThemeProvider>
</body>
</html>
);
}// app/dashboard/layout.tsx — 嵌套布局
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex h-screen">
{/* 侧边栏 */}
<aside className="w-64 border-r">
<DashboardNav />
</aside>
{/* 内容区域 */}
<main className="flex-1 overflow-auto">
<div className="p-6">{children}</div>
</main>
</div>
);
}// app/(auth)/layout.tsx — 路由组布局(括号不影响 URL)
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
// /login 和 /register 共享这个布局
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="w-full max-w-md">{children}</div>
</div>
);
}Server Components
默认行为
App Router 中的组件默认是 Server Components(服务端组件),它们在服务器上渲染,不会发送 JavaScript 到客户端。
// app/dashboard/page.tsx — Server Component(默认)
// 这个组件完全在服务器上执行,不包含客户端 JS
async function DashboardPage() {
// 可以直接访问数据库
const stats = await db.dashboard.findMany();
// 可以直接读取文件系统
const config = fs.readFileSync('./config.json', 'utf-8');
return (
<div>
<h1>仪表盘</h1>
<StatsGrid stats={stats} />
{/* 交互组件需要标记 'use client' */}
<RefreshButton />
</div>
);
}Server Components 的限制
// Server Components 不能使用:
// - useState, useReducer
// - useEffect, useLayoutEffect
// - useContext
// - 事件处理器(onClick, onChange 等)
// - 浏览器 API(window, document, localStorage)
// - 只能使用 React Server Components 的库
// 以下代码会在 Server Component 中报错:
function BadServerComponent() {
const [count, setCount] = useState(0); // Error!
useEffect(() => { /* ... */ }, []); // Error!
return <button onClick={() => setCount(1)}>Click</button>; // Error!
}Client Components
// app/components/RefreshButton.tsx — Client Component
'use client'; // 必须在文件顶部声明
import { useState } from 'react';
export function RefreshButton() {
const [loading, setLoading] = useState(false);
async function handleClick() {
setLoading(true);
try {
// 可以使用客户端 API
const res = await fetch('/api/refresh');
const data = await res.json();
// 使用 router.refresh() 刷新 Server Component 数据
window.location.reload();
} finally {
setLoading(false);
}
}
return (
<button onClick={handleClick} disabled={loading}>
{loading ? '刷新中...' : '刷新数据'}
</button>
);
}Server/Client 组件组合模式
// 模式 1:Server Component 包含 Client Component
// app/dashboard/page.tsx(Server Component)
async function DashboardPage() {
const data = await fetchData(); // 服务端获取数据
return (
<div>
<h1>仪表盘</h1>
{/* 将服务端数据通过 props 传递给客户端组件 */}
<InteractiveChart data={data} />
<FilterPanel filters={data.filters} />
</div>
);
}
// 模式 2:Client Component 作为容器,Server Component 作为子组件
// app/components/Tabs.tsx(Client Component)
'use client';
import { useState } from 'react';
export function Tabs({ children }: { children: React.ReactNode }) {
const [active, setActive] = useState(0);
const tabs = React.Children.toArray(children);
return (
<div>
<div className="flex gap-2">
{tabs.map((tab, i) => (
<button key={i} onClick={() => setActive(i)}>
Tab {i + 1}
</button>
))}
</div>
{tabs[active]}
</div>
);
}
// app/dashboard/page.tsx
function DashboardPage() {
return (
<Tabs>
<ServerTab1 /> {/* Server Component 可以作为 Client Component 的子组件 */}
<ServerTab2 />
</Tabs>
);
}Server Actions
Server Actions 是 Next.js 14 引入的特性,允许在组件中直接调用服务端函数,简化了表单提交和数据变更。
基础用法
// app/actions/device.ts
'use server'; // 必须声明
import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
// 创建设备
export async function createDevice(formData: FormData) {
const name = formData.get('name') as string;
const type = formData.get('type') as string;
// 服务端验证
if (!name || name.length < 2) {
return { error: '设备名称至少2个字符' };
}
// 数据库操作
await db.device.create({
data: { name, type },
});
// 刷新缓存
revalidatePath('/devices'); // 刷新 /devices 页面的缓存
// 或 revalidateTag('devices'); // 刷新带 'devices' 标签的缓存
return { success: true };
}
// 删除设备
export async function deleteDevice(id: string) {
await db.device.delete({ where: { id } });
revalidatePath('/devices');
}
// 带重定向的 Action
export async function updateDevice(id: string, formData: FormData) {
await db.device.update({
where: { id },
data: { name: formData.get('name') as string },
});
redirect(`/devices/${id}`); // 重定向
}// app/devices/page.tsx — 使用 Server Action
import { createDevice } from './actions';
function AddDeviceForm() {
return (
<form action={createDevice}>
<input name="name" placeholder="设备名称" required />
<select name="type">
<option value="sensor">传感器</option>
<option value="gateway">网关</option>
</select>
<button type="submit">添加设备</button>
</form>
);
}useActionState 和 useFormState
// app/devices/page.tsx — 带状态的 Server Action
'use client';
import { useActionState } from 'react';
import { createDevice } from './actions';
function AddDeviceForm() {
const [state, formAction, isPending] = useActionState(createDevice, null);
return (
<form action={formAction}>
<input name="name" placeholder="设备名称" />
{state?.error && <p className="text-red-500">{state.error}</p>}
{state?.success && <p className="text-green-500">添加成功!</p>}
<button type="submit" disabled={isPending}>
{isPending ? '添加中...' : '添加'}
</button>
</form>
);
}数据获取与缓存
fetch 与缓存策略
// app/dashboard/page.tsx
// fetch 默认会被缓存(GET 请求)
// 不缓存(每次请求都重新获取)
async function DashboardPage() {
const stats = await fetch('https://api.example.com/stats', {
cache: 'no-store',
}).then(r => r.json());
return <Stats data={stats} />;
}
// 缓存指定时间(ISR 效果)
async function BlogList() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }, // 每小时重新验证
}).then(r => r.json());
return <PostList posts={posts} />;
}
// 按标签刷新缓存
async function DeviceList() {
const devices = await fetch('https://api.example.com/devices', {
next: { tags: ['devices'] },
}).then(r => r.json());
return <DeviceGrid devices={devices} />;
}
// 在 Server Action 中刷新
// revalidateTag('devices'); // 刷新所有带 'devices' 标签的缓存并行数据获取
// app/dashboard/page.tsx
// Server Components 中可以并行获取数据(不会阻塞)
async function DashboardPage() {
// 三个请求并行执行
const [stats, recentDevices, alerts] = await Promise.all([
fetchStats(),
fetchRecentDevices(),
fetchAlerts(),
]);
return (
<div>
<StatsCard data={stats} />
<RecentDevices devices={recentDevices} />
<AlertList alerts={alerts} />
</div>
);
}数据库直接访问
// app/devices/[id]/page.tsx
import { db } from '@/lib/db';
async function DeviceDetail({ params }: { params: { id: string } }) {
// 直接在 Server Component 中访问数据库
const device = await db.device.findUnique({
where: { id: params.id },
include: {
sensors: true,
alerts: {
orderBy: { createdAt: 'desc' },
take: 10,
},
},
});
if (!device) {
notFound(); // 触发 not-found.tsx
}
return (
<div>
<h1>{device.name}</h1>
<SensorList sensors={device.sensors} />
<AlertHistory alerts={device.alerts} />
</div>
);
}Middleware
请求拦截
// middleware.ts — 放在项目根目录
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 认证检查
const token = request.cookies.get('auth-token');
if (pathname.startsWith('/admin') && !token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
// 已登录用户跳过登录页
if (pathname === '/login' && token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
// 添加自定义请求头
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-request-path', pathname);
return NextResponse.next({
request: { headers: requestHeaders },
});
}
// 配置匹配路径
export const config = {
matcher: [
'/admin/:path*',
'/dashboard/:path*',
'/login',
'/api/auth/:path*',
],
};API 路由
// app/api/devices/route.ts
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
// GET /api/devices
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '20');
const [devices, total] = await Promise.all([
db.device.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
}),
db.device.count(),
]);
return NextResponse.json({
data: devices,
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
});
}
// POST /api/devices
export async function POST(request: Request) {
const body = await request.json();
const { name, type } = body;
if (!name) {
return NextResponse.json(
{ error: '设备名称不能为空' },
{ status: 400 }
);
}
const device = await db.device.create({
data: { name, type: type || 'sensor' },
});
return NextResponse.json(device, { status: 201 });
}// app/api/devices/[id]/route.ts
// GET /api/devices/123
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const device = await db.device.findUnique({
where: { id: params.id },
});
if (!device) {
return NextResponse.json(
{ error: '设备不存在' },
{ status: 404 }
);
}
return NextResponse.json(device);
}
// DELETE /api/devices/123
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await db.device.delete({ where: { id: params.id } });
return NextResponse.json({ success: true });
}Metadata 与 SEO
// app/page.tsx
export const metadata: Metadata = {
title: '首页 - 我的应用',
description: '这是我的应用的首页描述',
keywords: ['Next.js', 'React', '全栈'],
authors: [{ name: '作者' }],
openGraph: {
title: '我的应用',
description: '应用描述',
url: 'https://example.com',
siteName: 'My App',
images: [{ url: 'https://example.com/og.png', width: 1200, height: 630 }],
locale: 'zh_CN',
type: 'website',
},
};
// 动态 Metadata
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return {
title: `${post.title} - 我的博客`,
description: post.excerpt,
};
}常见误区
误区 1:在 Server Component 中使用客户端 API
// Bad — Server Component 中使用 useState
function Page() {
const [data, setData] = useState(null); // Error!
useEffect(() => { fetch('/api/data') }, []); // Error!
}
// Good — 直接在 Server Component 中获取数据
async function Page() {
const data = await fetch('/api/data', { cache: 'no-store' }).then(r => r.json());
return <div>{data}</div>;
}
// 或使用 Client Component
// 'use client'
function ClientPage() {
const [data, setData] = useState(null);
useEffect(() => { fetch('/api/data').then(r => r.json()).then(setData); }, []);
}误区 2:过度使用 'use client'
// Bad — 所有组件都标记 'use client'
// 丧失了 Server Components 的优势
// Good — 只有需要交互的组件标记 'use client'
// Server Component 默认,需要交互时才用 Client Component误区 3:忽略缓存策略
// Bad — 默认缓存导致数据不更新
async function Page() {
const data = await fetch('/api/data'); // 默认永久缓存
}
// Good — 根据需求设置缓存策略
async function Page() {
const data = await fetch('/api/data', { cache: 'no-store' }); // 不缓存
// 或 next: { revalidate: 60 } // 60 秒后重新验证
}最佳实践总结
- 新项目使用 App Router —
app/目录是 Next.js 的推荐方式 - 默认 Server Component — 只在需要交互时标记
'use client' - 合理使用 Server Actions — 替代 API 路由处理数据变更
- 设置缓存策略 — 根据 data freshness 需求选择 cache/no-store/revalidate
- Middleware 处理认证 — 统一处理路由级别的权限控制
- 并行数据获取 — 使用 Promise.all 并行请求
