mcpField
N
nextjs-app-router-expert

nextjs-app-router-expert

github.com/curiositech开发257165 次安装2 天前更新
代码仓库

关于 nextjs-app-router-expert

用于构建和调试 Next.js 14/15 App Router 应用的开发技能,涵盖路由、React Server Components、数据获取、缓存、Server Actions、中间件和性能优化。

主要功能

  1. 01文件路由与高级路由模式
  2. 02React Server Components 与流式渲染
  3. 03数据获取和缓存策略
  4. 04Server Actions 与表单变更
  5. 05用于身份验证和重定向的中间件
  6. 06渲染与打包优化

适用场景

  1. 01启动 App Router 项目
  2. 02从 Pages Router 迁移
  3. 03构建模态框和并行路由流程
  4. 04排查 hydration 或 RSC 问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/curiositech/some_claude_skills --skill nextjs-app-router-expert

安装后在 Claude Code 中输入/nextjs-app-router-expert确认已加载。

附带文件.claude-plugin/plugin.json

详细介绍

Next.js App Router 专家

概述

精通 Next.js 14/15 App Router 架构、React Server Components(RSC)、Server Actions 以及现代全栈 React 开发。专长包括路由模式、数据获取策略、缓存、流式传输和部署优化。

适用场景

  • 使用 App Router 启动新的 Next.js 项目
  • 从 Pages Router 迁移到 App Router
  • 实现复杂的路由模式(并行路由、拦截路由)
  • 使用 RSC 和缓存优化数据获取
  • 设置 Server Actions 以执行变更操作
  • 配置中间件以处理身份验证和重定向
  • 调试 hydration 错误或 RSC 问题
  • 部署到 Vercel、Cloudflare 或可自托管环境

能力

路由架构
  • 使用 app/ 目录进行基于文件的路由
  • 动态路由([slug]、[...catchAll]、[[...optional]])
  • 使用路由组 (group) 整理路由
  • 并行路由 @modal、@sidebar
  • 拦截路由 (.)、(..)、(..)(..)
  • 为每个路由片段设置加载和错误边界
React Server Components
  • 服务端与客户端组件边界
  • 'use client' 指令的位置
  • 组合模式(服务端组件包裹客户端组件)
  • 使用 Suspense 边界进行流式传输
  • 渐进式渲染策略
数据获取
  • 使用 fetch() 自动去重
  • 缓存策略(force-cache、no-store、revalidate)
  • 使用 generateStaticParams() 进行静态生成
  • 增量静态再生成(ISR)
  • 使用 revalidatePath() / revalidateTag() 按需重新验证
Server Actions
  • 使用 'use server' 执行表单变更操作
  • 使用 useOptimistic 进行乐观更新
  • 渐进增强(无需 JS 也能运行)
  • 错误处理和验证
  • 变更操作后重定向
中间件与 Edge
  • 使用 middleware.ts 进行身份验证、重定向和重写
  • Edge Runtime 与 Node.js Runtime
  • 地理位置检测和条件路由
  • A/B 测试和功能标记
性能优化
  • 使用 next/image 优化图像
  • 使用 next/font 优化字体
  • 脚本加载策略
  • Bundle 分析和代码拆分
  • 部分预渲染(PPR)

依赖

可与以下工具配合使用:

  • react-performance-optimizer - React 专属性能模式
  • vercel-deployment - Vercel 部署配置
  • cloudflare-worker-dev - Edge 部署模式
  • postgresql-optimization - 用于 RSC 的数据库查询

示例

基本路由结构
app/
├── layout.tsx          # Root layout (required)
├── page.tsx            # Home page (/)
├── loading.tsx         # Loading UI
├── error.tsx           # Error boundary
├── not-found.tsx       # 404 page
├── blog/
│   ├── page.tsx        # /blog
│   └── [slug]/
│       ├── page.tsx    # /blog/:slug
│       └── loading.tsx # Per-route loading
└── (auth)/             # Route group (no URL impact)
    ├── login/
    │   └── page.tsx    # /login
    └── register/
        └── page.tsx    # /register
带数据获取的服务端组件
// app/posts/page.tsx
import { Suspense } from 'react';

async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 }, // ISR: revalidate every hour
  });
  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <main>
      <h1>Blog Posts</h1>
      <Suspense fallback={<PostsSkeleton />}>
        <PostList posts={posts} />
      </Suspense>
    </main>
  );
}
Server Action 表单
// app/contact/page.tsx
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';

async function submitContact(formData: FormData) {
  'use server';

  const email = formData.get('email') as string;
  const message = formData.get('message') as string;

  // Validate
  if (!email || !message) {
    throw new Error('Email and message required');
  }

  // Save to database
  await db.contacts.create({ email, message });

  // Revalidate and redirect
  revalidatePath('/contact');
  redirect('/contact/success');
}

export default function ContactPage() {
  return (
    <form action={submitContact}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button type="submit">Send</button>
    </form>
  );
}
并行路由(模态框模式)
app/
├── layout.tsx
├── page.tsx
├── @modal/
│   ├── default.tsx     # Empty state when no modal
│   └── (.)photo/[id]/
│       └── page.tsx    # Intercept /photo/[id] as modal
└── photo/[id]/
    └── page.tsx        # Full page when direct navigation
// app/layout.tsx
export default function Layout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <>
      {children}
      {modal}
    </>
  );
}
用于身份验证的中间件
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token');
  const isAuthPage = request.nextUrl.pathname.startsWith('/login');
  const isProtectedPage = request.nextUrl.pathname.startsWith('/dashboard');

  // Redirect authenticated users away from login
  if (isAuthPage && token) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  // Redirect unauthenticated users to login
  if (isProtectedPage && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/login'],
};
使用动态参数进行静态生成
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';

export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());

  return posts.map((post: { slug: string }) => ({
    slug: post.slug,
  }));
}

export async function generateMetadata({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);

  return {
    title: post?.title ?? 'Post Not Found',
    description: post?.excerpt,
  };
}

export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);

  if (!post) {
    notFound();
  }

  return (
    <article>
      <h1>{post.title}</h1>
      {/* NOTE: Always sanitize HTML content with DOMPurify before rendering */}
      <div>{post.content}</div>
    </article>
  );
}
使用 Suspense 进行流式传输
// app/dashboard/page.tsx
import { Suspense } from 'react';

export default function DashboardPage() {
  return (
    <div className="grid grid-cols-2 gap-4">
      {/* These load in parallel and stream in as ready */}
      <Suspense fallback={<CardSkeleton />}>
        <RevenueCard />
      </Suspense>
      <Suspense fallback={<CardSkeleton />}>
        <UsersCard />
      </Suspense>
      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  );
}

// Each component fetches its own data
async function RevenueCard() {
  const revenue = await getRevenue(); // Server-side fetch
  return <Card title="Revenue" value={revenue} />;
}

最佳实践

  1. 默认使用服务端组件 - 仅在需要交互功能时添加 'use client'
  2. 就近获取数据 - 在需要数据的组件中获取数据
  3. 使用 Suspense 边界 - 包裹异步组件以实现流式传输
  4. 善用缓存 - 使用 revalidate 和标签实现高效缓存
  5. 渐进增强 - Server Actions 无需 JavaScript 也能运行
  6. 使用路由组整理结构 - 使用 (folder) 进行整理,同时不影响 URL
  7. 为每个片段设置错误边界 - 为关键路由添加 error.tsx
  8. Metadata API - 使用 generateMetadata 实现动态 SEO
  9. 清理用户内容 - 渲染 HTML 时,始终使用 DOMPurify 或类似工具

常见陷阱

  • Hydration 不匹配 - 服务端和客户端渲染结果不同(日期、随机值)
  • 过度使用 'use client' - 将客户端边界推得过高
  • 瀑布式获取 - 未并行执行相互独立的数据获取
  • 缺少加载状态 - 忘记添加 loading.tsx 或 Suspense 边界
  • 数据过时 - 变更操作后未使缓存失效
  • 客户端 Bundle 过大 - 在客户端组件中导入仅限服务端使用的代码
  • XSS 漏洞 - 渲染未经清理的用户输入 HTML

评论

支持 Markdown