尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Next.js App Router 模式完全指南:agents 仓库 frontend-mobile-development 插件的 8 大核心模式与缓存策略

Next.js App Router 模式完全指南:agents 仓库 frontend-mobile-development 插件的 8 大核心模式与缓存策略 Next.js App Router 模式完全指南agents 仓库 frontend-mobile-development 插件的 8 大核心模式与缓存策略【免费下载链接】agentsMulti-harness agentic plugin marketplace for Claude Code, Codex, Cursor, OpenCode, GitHub Copilot, and Google Antigravity项目地址: https://gitcode.com/GitHub_Trending/agents24/agents本文基于 agents 多 harness Agent 插件市场中frontend-mobile-development插件的nextjs-app-router-patternsSkill 参考文档 references/details.md系统讲解 Next.js App Router 的 8 大生产级模式——Server Components 数据获取、Client Components、Server Actions、Parallel Routes、Intercepting Routes、Suspense 流式渲染、Route Handlers 与 Metadata/SEO以及配套的数据缓存策略。读完本文你将掌握从组件边界划分、异步数据流到缓存失效控制的完整 App Router 实战方案并能理解这套知识在 Agent 插件体系中的组织方式。文档定位Agent Skill 中的渐进式知识披露nextjs-app-router-patterns是frontend-mobile-development插件版本 1.2.3类别 development见 plugin.json 与 .claude-plugin/marketplace.json 中的登记条目内置的 4 个 Skill 之一遵循 Anthropic Agent Skills 规范的渐进式披露progressive disclosure结构plugins/frontend-mobile-development/skills/nextjs-app-router-patterns/ ├── SKILL.md # 导航层渲染模式表、文件约定、Quick Start、最佳实践 └── references/ └── details.md # 详细层8 大模式 缓存策略的完整代码示例SKILL.md 明确说明了两层分工Detailed pattern documentation lives inreferences/details.md. Read that file when the navigation tier above is insufficient.——即 Agent 先加载轻量导航层只有当需要深入实现时才读取本文档。这正是该文档作为详细参考层的定位导航层负责何时用本文档负责怎么写。安装该 Skill 的方式依据 README.md 的 Quick start 章节# Claude Code安装整个插件 /plugin marketplace add wshobson/agents /plugin install frontend-mobile-development # 或仅安装单个 Skill无需克隆仓库、无需生成步骤 gh skill install wshobson/agents npx skills add wshobson/agents --skill nextjs-app-router-patterns该插件配套的 frontend-developer agent 声明覆盖 Next.js 15 App Router with Server Components and Client Components、React Server Components (RSC) and streaming patterns、Advanced routing with parallel routes, intercepting routes, and route handlers本文档即为该能力声明对应的知识实现。适用前提文档声明面向 Next.js 14SKILL.md frontmatter 描述而其中所有页面组件均采用params: Promise.../searchParams: Promise...的异步参数风格即 Next.js 15 引入的 Promise 化路由参数写法。在 14 及以下版本中使用这些示例时params与searchParams是同步对象需要去掉await。核心概念渲染模式与文件约定在进入具体模式之前先继承 SKILL.md 导航层的两张核心表它们是理解后文 8 个模式的坐标系。渲染模式选择模式运行位置适用场景Server Components仅服务端数据获取、重计算、密钥访问Client Components浏览器交互、hooks、浏览器 APIStatic构建时很少变更的内容Dynamic请求时个性化或实时数据Streaming渐进式大页面、慢数据源App 目录文件约定app/ ├── layout.tsx # Shared UI wrapper ├── page.tsx # Route UI ├── loading.tsx # Loading UI (Suspense) ├── error.tsx # Error boundary ├── not-found.tsx # 404 UI ├── route.ts # API endpoint ├── template.tsx # Re-mounted layout ├── default.tsx # Parallel route fallback └── opengraph-image.tsx # OG image generation后文的模式 4 用到default.tsx与loading.tsx模式 7 用到route.ts与本约定表一一对应。Pattern 1Server Components 数据获取原文档的第一个模式展示了电商商品列表页的完整数据流页面组件作为 Server Component 读取查询参数用Suspense包裹慢数据区子组件直接await fetch取数。// app/products/page.tsx import { Suspense } from react import { ProductList, ProductListSkeleton } from /components/products import { FilterSidebar } from /components/filters interface SearchParams { category?: string sort?: price | name | date page?: string } export default async function ProductsPage({ searchParams, }: { searchParams: PromiseSearchParams }) { const params await searchParams return ( div classNameflex gap-8 FilterSidebar / Suspense key{JSON.stringify(params)} fallback{ProductListSkeleton /} ProductList category{params.category} sort{params.sort} page{Number(params.page) || 1} / /Suspense /div ) } // components/products/ProductList.tsx - Server Component async function getProducts(filters: ProductFilters) { const res await fetch( ${process.env.API_URL}/products?${new URLSearchParams(filters)}, { next: { tags: [products] } } ) if (!res.ok) throw new Error(Failed to fetch products) return res.json() } export async function ProductList({ category, sort, page }: ProductFilters) { const { products, totalPages } await getProducts({ category, sort, page }) return ( div div classNamegrid grid-cols-3 gap-4 {products.map((product) ( ProductCard key{product.id} product{product} / ))} /div Pagination currentPage{page} totalPages{totalPages} / /div ) }见 details.md三个值得注意的要点key{JSON.stringify(params)}技巧给Suspense边界设置随查询参数变化的 key使得 URL 查询串变化切换分类、排序、页码时边界内容整体重建并重新触发 fallback。若不设 key客户端导航改参数时 React 会尝试复用边界内的组件树可能跳过重新挂起/流式过程。数据获取就近原则getProducts定义在使用数据的地方与 SKILL.md 最佳实践 Colocate data fetching - Fetch data where its used 对应。缓存标签预埋fetch的next: { tags: [products] }为该响应打上标签是后文缓存策略一节中revalidateTag(products)精确失效的前提。Pattern 2Client Components 与 use client交互型组件在文件顶部声明use client成为客户端组件子树的根// components/products/AddToCartButton.tsx use client import { useState, useTransition } from react import { addToCart } from /app/actions/cart export function AddToCartButton({ productId }: { productId: string }) { const [isPending, startTransition] useTransition() const [error, setError] useStatestring | null(null) const handleClick () { setError(null) startTransition(async () { const result await addToCart(productId) if (result.error) { setError(result.error) } }) } return ( div button onClick{handleClick} disabled{isPending} classNamebtn-primary {isPending ? Adding... : Add to Cart} /button {error p classNametext-red-500 text-sm{error}/p} /div ) }见 details.md该示例与 Pattern 3 的 Server Action 形成完整闭环addToCart是app/actions/cart.ts导出的 server action客户端组件通过startTransition包裹调用isPending自动反映请求进行状态按钮禁用 文案切换错误则通过返回值{ error }回传并在本地 state 中渲染。这正是 Next.js 推荐的最小客户端边界——只有按钮这一叶子节点带use client其余部分留在服务端。注意客户端组件只能接收可序列化 props此处仅productId: string这也是 SKILL.md Donts 中 Dont pass serializable data - Server → Client boundary limitations 的具体体现原文强调 Server→Client 边界的数据必须可序列化。Pattern 3Server ActionsServer Actions 是 Next.js App Router 中原生的服务端变更mutation机制用use server指令标记模块或函数// app/actions/cart.ts use server; import { revalidateTag } from next/cache; import { cookies } from next/headers; import { redirect } from next/navigation; export async function addToCart(productId: string) { const cookieStore await cookies(); const sessionId cookieStore.get(session)?.value; if (!sessionId) { redirect(/login); } try { await db.cart.upsert({ where: { sessionId_productId: { sessionId, productId } }, update: { quantity: { increment: 1 } }, create: { sessionId, productId, quantity: 1 }, }); revalidateTag(cart); return { success: true }; } catch (error) { return { error: Failed to add item to cart }; } } export async function checkout(formData: FormData) { const address formData.get(address) as string; const payment formData.get(payment) as string; // Validate if (!address || !payment) { return { error: Missing required fields }; } // Process order const order await processOrder({ address, payment }); // Redirect to confirmation redirect(/orders/${order.id}/confirmation); }见 details.md该示例覆盖了 Server Actions 的三类关键能力认证检查与服务端重定向await cookies()Promise 风格Next.js 15读取会话未登录时redirect(/login)在服务端直接抛出重定向不产生一次客户端往返。幂等写入upsert以联合唯一键sessionId_productId定位购物车行重复点击加入购物车时递增数量而非报错。变更后缓存失效revalidateTag(cart)在数据写入后立即使打了cart标签的缓存失效保证随后渲染的页面读到新数据——这与 Pattern 1 中fetch的tags配置构成端到端的缓存闭环。checkout则演示了 Server Action 作为form action{checkout}处理器的形态直接接收FormData校验失败返回结构化错误对象成功则redirect到订单确认页303 模式避免重复提交。Pattern 4Parallel Routes并行路由Parallel Routes允许同一布局中的多个插槽独立加载、独立展示各自的 loading 状态插槽用name目录表示// app/dashboard/layout.tsx export default function DashboardLayout({ children, analytics, team, }: { children: React.ReactNode analytics: React.ReactNode team: React.ReactNode }) { return ( div classNamedashboard-grid main{children}/main aside classNameanalytics-panel{analytics}/aside aside classNameteam-panel{team}/aside /div ) } // app/dashboard/analytics/page.tsx export default async function AnalyticsSlot() { const stats await getAnalytics() return AnalyticsChart data{stats} / } // app/dashboard/analytics/loading.tsx export default function AnalyticsLoading() { return ChartSkeleton / } // app/dashboard/team/page.tsx export default async function TeamSlot() { const members await getTeamMembers() return TeamList members{members} / }见 details.md目录到布局 prop 的映射规则是app/dashboard/analytics/下的页面组件渲染结果注入 layout 的analyticspropteam/注入teamprop主路由仍是children。每个插槽可以拥有自己的loading.tsx对应文件约定表中的 Parallel route fallback 条目当该插槽数据未就绪时只挂起该面板而不阻塞主内容。这是仪表盘类布局的标准解法慢的 analytics 面板不会拖累主区域渲染。Pattern 5Intercepting Routes模态框模式拦截路由Intercepting Routes让同一段 UI 既能作为模态框在当前页上叠出也能作为独立全页存在实现同一数据、两种呈现// File structure for photo modal // app/ // ├── modal/ // │ ├── (.)photos/[id]/page.tsx # Intercept // │ └── default.tsx // ├── photos/ // │ └── [id]/page.tsx # Full page // └── layout.tsx // app/modal/(.)photos/[id]/page.tsx import { Modal } from /components/Modal import { PhotoDetail } from /components/PhotoDetail export default async function PhotoModal({ params, }: { params: Promise{ id: string } }) { const { id } await params const photo await getPhoto(id) return ( Modal PhotoDetail photo{photo} / /Modal ) } // app/photos/[id]/page.tsx - Full page version export default async function PhotoPage({ params, }: { params: Promise{ id: string } }) { const { id } await params const photo await getPhoto(id) return ( div classNamephoto-page PhotoDetail photo{photo} / RelatedPhotos photoId{id} / /div ) } // app/layout.tsx export default function RootLayout({ children, modal, }: { children: React.ReactNode modal: React.ReactNode }) { return ( html body {children} {modal} /body /html ) }见 details.md从文件结构看关键机制(.)语法是拦截路由的核心app/modal/(.)photos/[id]/page.tsx中的(.)表示拦截相对当前层级的路由——当用户在/photos/123页内导航如点击列表中的另一张照片时匹配到的渲染进入modal插槽以模态框形式叠出而不离开当前页面当从外部直接访问/photos/123时则正常落入全页版本。default.tsx是modal插槽的空态回退没有路由被拦截时插槽渲染default.tsx通常为空组件保证布局中{modal}位置不报错。两种形态共享PhotoDetail组件模态框版本精简为纯详情全页版本额外渲染RelatedPhotos实现了 UI 复用且互不污染。Pattern 6Suspense 流式渲染流式Streaming让页面先出快的再流慢的把不同延迟的数据源放进各自独立的Suspense边界// app/product/[id]/page.tsx import { Suspense } from react export default async function ProductPage({ params, }: { params: Promise{ id: string } }) { const { id } await params // This data loads first (blocking) const product await getProduct(id) return ( div {/* Immediate render */} ProductHeader product{product} / {/* Stream in reviews */} Suspense fallback{ReviewsSkeleton /} Reviews productId{id} / /Suspense {/* Stream in recommendations */} Suspense fallback{RecommendationsSkeleton /} Recommendations productId{id} / /Suspense /div ) } // These components fetch their own data async function Reviews({ productId }: { productId: string }) { const reviews await getReviews(productId) // Slow API return ReviewList reviews{reviews} / } async function Recommendations({ productId }: { productId: string }) { const products await getRecommendations(productId) // ML-based, slow return ProductCarousel products{products} / }见 details.md结构上的分工非常清晰阻塞数据getProduct在组件顶层await决定页面的首帧——头部信息必须齐备才能渲染主体非关键慢数据评论 API、基于 ML 的推荐下沉到Reviews、Recommendations内部自行 fetch并各自包在独立边界中。哪个先返回哪个先替换 skeleton互不等待。这与 Pattern 1 的差别在于Pattern 1 是单个边界包裹整块数据配合key做参数级重建Pattern 6 是多个边界按数据源延迟分层配合渐进式水合。两者可组合使用。Pattern 7Route HandlersAPI 路由需要纯 JSON 接口第三方调用、webhook、非 React 消费端时用app/api/**/route.ts导出 HTTP 方法处理器// app/api/products/route.ts import { NextRequest, NextResponse } from next/server; export async function GET(request: NextRequest) { const searchParams request.nextUrl.searchParams; const category searchParams.get(category); const products await db.product.findMany({ where: category ? { category } : undefined, take: 20, }); return NextResponse.json(products); } export async function POST(request: NextRequest) { const body await request.json(); const product await db.product.create({ data: body, }); return NextResponse.json(product, { status: 201 }); } // app/api/products/[id]/route.ts export async function GET( request: NextRequest, { params }: { params: Promise{ id: string } }, ) { const { id } await params; const product await db.product.findUnique({ where: { id } }); if (!product) { return NextResponse.json({ error: Product not found }, { status: 404 }); } return NextResponse.json(product); }见 details.md要点集合路由处理GET带可选category查询过滤、take: 20分页上限与POST201 返回创建结果动态段路由处理单资源GET未找到时返回规范化的 404 JSON 而非抛错。注意动态段处理器的第二参数同样是params: Promise{ id: string }需await解包与页面组件保持一致的 15 风格。Pattern 8Metadata 与 SEO动态页面的 SEO 元数据由generateMetadata异步生成配合generateStaticParams预渲染与notFound()兜底// app/products/[slug]/page.tsx import { Metadata } from next import { notFound } from next/navigation type Props { params: Promise{ slug: string } } export async function generateMetadata({ params }: Props): PromiseMetadata { const { slug } await params const product await getProduct(slug) if (!product) return {} return { title: product.name, description: product.description, openGraph: { title: product.name, description: product.description, images: [{ url: product.image, width: 1200, height: 630 }], }, twitter: { card: summary_large_image, title: product.name, description: product.description, images: [product.image], }, } } export async function generateStaticParams() { const products await db.product.findMany({ select: { slug: true } }) return products.map((p) ({ slug: p.slug })) } export default async function ProductPage({ params }: Props) { const { slug } await params const product await getProduct(slug) if (!product) notFound() return ProductDetail product{product} / }见 details.md三个细节generateMetadata与页面组件共享同一数据源都是getProduct(slug)保证title/OG 卡与页面正文一致查不到商品时 metadata 返回空对象、页面组件调用notFound()触发not-found.tsx对应文件约定表。generateStaticParams构建时枚举全部 slug使这批商品页进入 SSG/ISR 轨道未枚举的 slug 在运行时按需动态渲染配合 ISR 缓存见下节。OG 图片显式声明1200×630符合社交卡片分享的标准比例文件约定表中的opengraph-image.tsx则是其文件式替代方案程序化生成 OG 图。缓存策略从 no-store 到标签化失效文档最后一节details.md把 App Router 的缓存手段汇总为五种组合// No cache (always fresh) fetch(url, { cache: no-store }); // Cache forever (static) fetch(url, { cache: force-cache }); // ISR - revalidate after 60 seconds fetch(url, { next: { revalidate: 60 } }); // Tag-based invalidation fetch(url, { next: { tags: [products] } }); // Invalidate via Server Action (use server); import { revalidateTag, revalidatePath } from next/cache; export async function updateProduct(id: string, data: ProductData) { await db.product.update({ where: { id }, data }); revalidateTag(products); revalidatePath(/products); }按新鲜度—成本权衡理解这五档配置行为典型场景cache: no-store每次请求回源不缓存会话数据、高频变化接口cache: force-cache永久缓存直至部署静态资源、不变内容next: { revalidate: 60 }ISR60 秒后请求触发异步再生商品列表、内容页next: { tags: [products] }打标签等待显式失效被多处引用、需要精确控制失效时机的数据revalidateTag/revalidatePath在 Server Action 中主动失效写操作后立即让读路径可见新数据最后一个片段把前文所有模式串成闭环updateProduct作为use serveraction 写库后同时执行revalidateTag(products)按 Pattern 1 中fetch打的标签失效数据缓存和revalidatePath(/products)按路径失效页面 HTML 缓存。写操作与缓存失效集中在同一个 Server Action 中避免了数据已更新但页面仍展示旧缓存的一致性窗口。选择策略的经验路径先用tags精确圈定哪些数据会变再在对应的 Server Action如 Pattern 3 的addToCart中的revalidateTag(cart)里失效对整页级的粗粒度失效用revalidatePath对只读且可容忍短暂滞后的读路径叠加revalidate秒数作为兜底。最佳实践清单继承自导航层SKILL.md 的 Dos/Donts 是对 8 个模式的收敛总结逐条对应上文示例Dos从 Server Components 起步——确需交互时才加use clientPattern 2 中仅按钮是客户端组件数据获取就近放置——在消费它的地方 fetchPattern 1、6 均如此使用 Suspense 边界——为慢数据启用流式Pattern 1、6利用 Parallel Routes——让各面板有独立 loading 状态Pattern 4使用 Server Actions——带渐进增强能力的变更操作Pattern 3Donts不要跨边界传不可序列化数据——Server → Client 只允许可序列化 propsPattern 2 仅传productId字符串不要在 Server Components 中使用 hooks——无useState/useEffect不要在 Client Components 中 fetch——用 Server Components 或 React Query 承担数据获取不要过度嵌套 layouts——每层 layout 都增加组件树深度不要忽略 loading 状态——始终提供loading.tsx或SuspensefallbackPattern 4 的analytics/loading.tsx即示例相关资源索引资源路径本文核心参考文档8 模式 缓存策略plugins/frontend-mobile-development/skills/nextjs-app-router-patterns/references/details.mdSkill 导航层渲染模式表、文件约定、Quick Start、Do/Dontsplugins/frontend-mobile-development/skills/nextjs-app-router-patterns/SKILL.md配套前端 Agent 定义plugins/frontend-mobile-development/agents/frontend-developer.md插件元数据plugins/frontend-mobile-development/.claude-plugin/plugin.json市场目录登记.claude-plugin/marketplace.json插件市场安装与多 harness 支持说明README.md以上模式代码均可直接复制到 Next.js 15 项目TypeScript中使用示例中的db假定为 Prisma 或 Drizzle 等数据库客户端process.env.API_URL等环境变量需按实际部署配置。若你在 Claude Code、Codex、Cursor、OpenCode、Antigravity 或 Copilot 中使用 agents 插件市场安装frontend-mobile-development插件后即可让 Agent 在这些模式上按本文路径直接查阅参考文档。【免费下载链接】agentsMulti-harness agentic plugin marketplace for Claude Code, Codex, Cursor, OpenCode, GitHub Copilot, and Google Antigravity项目地址: https://gitcode.com/GitHub_Trending/agents24/agents创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表