关于 nextjs-app-router-expert
用于构建和调试 Next.js 14/15 App Router 应用的开发技能,涵盖路由、React Server Components、数据获取、缓存、Server Actions、中间件和性能优化。
主要功能
- 01文件路由与高级路由模式
- 02React Server Components 与流式渲染
- 03数据获取和缓存策略
- 04Server Actions 与表单变更
- 05用于身份验证和重定向的中间件
- 06渲染与打包优化
适用场景
- 01启动 App Router 项目
- 02从 Pages Router 迁移
- 03构建模态框和并行路由流程
- 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} />;
}
最佳实践
- 默认使用服务端组件 - 仅在需要交互功能时添加
'use client' - 就近获取数据 - 在需要数据的组件中获取数据
- 使用 Suspense 边界 - 包裹异步组件以实现流式传输
- 善用缓存 - 使用
revalidate和标签实现高效缓存 - 渐进增强 - Server Actions 无需 JavaScript 也能运行
- 使用路由组整理结构 - 使用
(folder)进行整理,同时不影响 URL - 为每个片段设置错误边界 - 为关键路由添加
error.tsx - Metadata API - 使用
generateMetadata实现动态 SEO - 清理用户内容 - 渲染 HTML 时,始终使用 DOMPurify 或类似工具
常见陷阱
- Hydration 不匹配 - 服务端和客户端渲染结果不同(日期、随机值)
- 过度使用 'use client' - 将客户端边界推得过高
- 瀑布式获取 - 未并行执行相互独立的数据获取
- 缺少加载状态 - 忘记添加
loading.tsx或 Suspense 边界 - 数据过时 - 变更操作后未使缓存失效
- 客户端 Bundle 过大 - 在客户端组件中导入仅限服务端使用的代码
- XSS 漏洞 - 渲染未经清理的用户输入 HTML
评论
支持 Markdown