关于 nextjs-app-router
面向 Next.js App Router(v13+)开发的参考技能,涵盖路由、组件、数据处理及生产环境实践,供 Next.js 开发者查阅。
主要功能
- 01App Router 文件约定
- 02服务端与客户端组件指南
- 03数据获取与缓存模式
- 04用于数据修改的 Server Actions
- 05安全与错误处理指南
- 06构建和性能检查清单
适用场景
- 01为路由选择组件类型
- 02规划获取数据的缓存行为
- 03配置生产环境安全设置
- 04排查常见 App Router 问题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill nextjs-app-router安装后在 Claude Code 中输入/nextjs-app-router确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
advanced.mdquick-ref/data-fetching.mdfile-conventions.md详细介绍
Next.js App Router 核心
完整参考:有关 WebSocket 集成、Socket.IO、Server-Sent Events、TanStack Query 实时同步以及 Vercel/Pusher 模式,请参阅 advanced.md。
深入了解:使用
mcp__documentation__fetch_docs,并将 technology 设为:nextjs,以获取全面的文档。
文件约定
| 文件 | 用途 |
|---|---|
page.tsx | 路由 UI(路由必需) |
layout.tsx | 共享布局,可保留状态 |
loading.tsx | 加载 UI(Suspense) |
error.tsx | 错误边界 |
not-found.tsx | 404 页面 |
route.ts | API 端点 |
组件类型
服务端组件(默认)
// No 'use client' - runs on server
async function Page() {
const data = await db.query(...); // Direct DB access
return <div>{data.name}</div>;
}
客户端组件
'use client'
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
数据获取
// Server Component
async function Page() {
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache', // Static (default)
// cache: 'no-store', // Dynamic
// next: { revalidate: 60 } // ISR
});
return <Component data={await data.json()} />;
}
服务端操作
'use server'
export async function createItem(formData: FormData) {
await db.items.create({ name: formData.get('name') });
revalidatePath('/items');
}
选择规则
| 场景 | 使用 |
|---|---|
| 交互式 UI | 'use client' |
| 数据获取 | 服务端组件 |
| 表单变更 | 服务端操作 |
| 共享状态 | 客户端组件 |
生产就绪
安全配置
// next.config.js - Security headers
const securityHeaders = [
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-XSS-Protection', value: '1; mode=block' },
{ key: 'Referrer-Policy', value: 'origin-when-cross-origin' },
{
key: 'Content-Security-Policy',
value: `default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';`,
},
];
module.exports = {
async headers() {
return [{ source: '/:path*', headers: securityHeaders }];
},
};
// Server-side secrets (never exposed to client)
// Use server components or API routes
async function SecureComponent() {
const apiKey = process.env.API_SECRET_KEY; // Server only
const data = await fetch(url, { headers: { Authorization: apiKey } });
return <div>{/* render data */}</div>;
}
// Environment variables
// NEXT_PUBLIC_* = exposed to client (careful!)
// Other vars = server-side only
错误处理
// app/error.tsx - Error boundary
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log to error reporting service
captureException(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/global-error.tsx - Root error boundary
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</body>
</html>
);
}
性能优化
// Dynamic imports for client components
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('@/components/Chart'), {
loading: () => <Skeleton />,
ssr: false, // Client-only if needed
});
// Image optimization
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // For LCP images
placeholder="blur"
blurDataURL={blurHash}
/>
// Font optimization
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap' });
缓存策略
// Static data (cached indefinitely)
async function StaticPage() {
const data = await fetch(url, { cache: 'force-cache' });
}
// Dynamic data (never cached)
async function DynamicPage() {
const data = await fetch(url, { cache: 'no-store' });
}
// ISR (revalidate every 60 seconds)
async function ISRPage() {
const data = await fetch(url, { next: { revalidate: 60 } });
}
// On-demand revalidation
import { revalidatePath, revalidateTag } from 'next/cache';
async function updateData() {
'use server';
await db.update(...);
revalidatePath('/posts'); // Revalidate specific path
revalidateTag('posts'); // Revalidate by tag
}
中间件安全
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Rate limiting header for upstream proxy
const response = NextResponse.next();
// Auth check
const token = request.cookies.get('session');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// CORS for API routes
if (request.nextUrl.pathname.startsWith('/api')) {
response.headers.set('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGIN!);
}
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
监控指标
| 指标 | 告警阈值 |
|---|---|
| 最大内容绘制时间(LCP) | > 2.5s |
| 首次输入延迟(FID) | > 100ms |
| 累积布局偏移(CLS) | > 0.1 |
| 首字节时间(TTFB) | > 800ms |
| 服务端组件渲染时间 | > 200ms |
| 构建体积增幅 | > 10% |
构建与部署
// next.config.js - Production optimizations
module.exports = {
output: 'standalone', // For Docker deployments
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'cdn.example.com' },
],
},
experimental: {
// Enable if using PPR
ppr: true,
},
};
# Production build with analysis
ANALYZE=true npm run build
# Check bundle size
npx @next/bundle-analyzer
检查清单
- 已配置安全标头
- 已定义 CSP 策略
- NEXT_PUBLIC_* 变量中不包含机密信息
- 已在路由级别设置错误边界
- 已设置全局错误边界
- 使用 next/image 优化图像
- 使用 next/font 优化字体
- 对大型组件使用动态导入
- 为每个路由制定缓存策略
- 使用中间件进行身份验证/速率限制
- 已监控 Core Web Vitals
- 使用独立输出进行容器化
不适用此技能的情况
此技能适用于 Next.js App Router(v13+)。以下情况请勿使用:
- 不使用 Next.js 的 React:请改用
frontend-react技能 - Next.js Pages Router(v12 及以下):请查阅 KB 获取迁移指南
- Nuxt.js(Vue 元框架):请改用
nuxt3技能 - Remix(React 元框架):请改用
remix技能 - SvelteKit:请改用
sveltekit技能 - Astro:请改用
astro技能 - 仅提供 API 的后端:请考虑改用
nestjs或fastapi
反模式
| 反模式 | 错误原因 | 正确做法 |
|---|---|---|
| 到处使用 'use client' | 无法发挥服务端组件的优势,还会增大打包体积 | 仅在需要交互的组件中使用 'use client' |
| 在客户端组件中获取数据 | 会产生瀑布式请求、不支持 SSR,且 SEO 效果不佳 | 在服务端组件中获取数据,或使用服务端操作 |
| 不使用 loading.tsx | 数据获取期间的用户体验较差 | 创建 loading.tsx 来处理路由级别的加载状态 |
| 在 NEXT_PUBLIC_* 中放置机密信息 | 信息会暴露给客户端,存在安全风险 | 使用仅限服务端的环境变量或服务端组件 |
| 忽略缓存策略 | 性能不佳或数据过时 | 显式设置 cache:'force-cache'、'no-store' 或 revalidate |
| 没有错误边界 | 未捕获的错误会导致整个应用崩溃 | 在路由级别和根级别添加 error.tsx |
| fetch() 未设置缓存选项 | 缓存行为不可预测 | 始终指定缓存或重新验证策略 |
| 使用 useEffect 获取数据 | 仅在客户端执行,不支持 SSR | 使用搭配 async/await 的服务端组件 |
快速故障排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| “无法在 Server Component 中使用 useState” | 缺少 'use client' 指令 | 在文件顶部添加 'use client' |
| 变更后数据未更新 | 缓存未重新验证 | 在 Server Action 中使用 revalidatePath() 或 revalidateTag() |
| 水合不匹配错误 | 服务端和客户端的渲染结果不同 | 确保数据一致,并检查 Date/随机值 |
| 在组件中“无法访问 cookies” | cookies 只能在 Server Component/Action 中使用 | 将逻辑移至 Server Component 或 API 路由 |
| 构建失败,提示“动态服务端用法” | 在静态路由中使用动态 API | 添加 export const dynamic = 'force-dynamic' |
| 图片未优化 | 未使用 next/image | 将 |
| 页面加载缓慢 | 客户端包过大 | 使用动态导入,并检查包分析器 |
| API 路由出现 CORS 错误 | route.ts 中缺少响应头 | 在路由处理程序中添加 CORS 响应头 |
参考文档
评论
支持 Markdown