mcpField
N
nextjs-app-router

nextjs-app-router

github.com/claude-dev-suite开发4638 次安装3 天前更新
代码仓库

关于 nextjs-app-router

面向 Next.js App Router(v13+)开发的参考技能,涵盖路由、组件、数据处理及生产环境实践,供 Next.js 开发者查阅。

主要功能

  1. 01App Router 文件约定
  2. 02服务端与客户端组件指南
  3. 03数据获取与缓存模式
  4. 04用于数据修改的 Server Actions
  5. 05安全与错误处理指南
  6. 06构建和性能检查清单

适用场景

  1. 01为路由选择组件类型
  2. 02规划获取数据的缓存行为
  3. 03配置生产环境安全设置
  4. 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.md
quick-ref/
data-fetching.md
file-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.tsx404 页面
route.tsAPI 端点

组件类型

服务端组件(默认)
// 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将 README 配图 替换为 next/image 中的 README 配图
页面加载缓慢客户端包过大使用动态导入,并检查包分析器
API 路由出现 CORS 错误route.ts 中缺少响应头在路由处理程序中添加 CORS 响应头

参考文档

评论

支持 Markdown