关于 web-meta-framework-nextjs
面向使用 Next.js App Router 的开发者,提供路由、组件边界、缓存和 Server Actions 的实现模式。内容涵盖版本差异与常见陷阱,不适用于 Pages Router。
主要功能
- 01Server 与 Client 组件边界
- 02路由、布局与动态段模式
- 03Server Actions 与数据变更
- 04缓存失效与重新验证
- 05流式渲染及元数据设置
适用场景
- 01规划 app/ 目录路由与布局
- 02选择组件在服务器或浏览器运行
- 03实现表单操作并刷新缓存
- 04为页面添加流式加载与 SEO 元数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-meta-framework-nextjs安装后在 Claude Code 中输入/web-meta-framework-nextjs确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/cookies.mdcore.mdevent-handlers.mdmetadata.mdmutations.mdoptimistic.mdparallel-routes.mdrevalidation.mdroute-groups.mdserver-actions.mdserver-components.mdstreaming.mdversions.md详细介绍
Next.js App Router 模式
**快速指南:**Server Components 是默认选项,而
"use client"需要显式启用,因此应让客户端组件保持精简,并放在组件树的叶子节点。读取操作在 Server Components 中执行;写入操作在 Server Actions 中执行,后者是公开的 HTTP 端点,并会自行重新验证缓存。以下两项版本信息会影响后续答案:从 15 开始,params、searchParams、cookies()和headers()都是 Promise;在 16 中,middleware.ts改为proxy.ts,revalidateTag()接受一个cacheLife配置,而experimental_ppr改为cacheComponents。
详细资源:
- examples/core.md — 文件约定、动态路由、布局、错误和未找到页面 UI、仅限服务端的代码
- examples/server-components.md — Server/Client 边界、组合、使用 Suspense 实现流式传输
- examples/metadata.md — 标题模板、
generateMetadata、Open Graph 和 Twitter 卡片 - examples/parallel-routes.md —
@slot文件夹、拦截路由、模态框模式 - examples/route-groups.md — 每个分区使用一个布局,且不更改 URL
- examples/versions.md — PPR、Turbopack、类型化路由、
after()、插桩以及 v16 迁移 - examples/server-actions.md — Action 定义、表单 Action、验证、授权、待处理状态
- examples/mutations.md — 同时运行彼此独立的 Action
- examples/revalidation.md — 按路径与按标签失效、为 fetch 添加标签
- examples/optimistic.md — 使用
useOptimistic提供即时反馈 - examples/event-handlers.md — 使用
useTransition从点击处理程序调用 Action - examples/streaming.md — 长时间运行的 Action 的进度更新
- examples/cookies.md — 在 Action 中读取和写入 Cookie
- reference.md — 决策树、路由段配置、Hook 签名、各版本变更
适用路径
- 使用 Next.js 16 — 缓存需要显式启用,且多个 API 已重命名;在复制任何缓存、中间件或并行路由模式之前,请先阅读 examples/versions.md 中关于迁移的部分。
- 使用 Pages Router(
pages/) — 适用的是另一套约定,上述文件均未对此进行说明。
<critical_requirements>
编写 Next.js 代码前
将组件编写为 Server Components,仅在需要状态、效果或事件处理程序时添加 "use client"。 该指令决定代码是否会发送到浏览器,因此将它放在组件树的叶节点处,可以缩小打包体积,并让密钥留在服务器上。
在任何涉及密钥的模块中导入 server-only。 这样,构建会在有问题的导入处失败,而不是把密钥发送到浏览器。
为数据量大的路由提供 loading.tsx,并为独立的区域分别设置 <Suspense>。 loading.tsx 会自动用边界包裹页面;每个区域各设一个边界,可以避免最慢的请求阻塞最快的请求。
通过 metadata 对象或 generateMetadata 设置 SEO。 Next.js 会对这些标签去重并排序,而手动编写的 <head> 标记不会。
使用 'use server' 标记 Server Actions — 放在文件顶部,或异步函数顶部。 否则,该函数会被打包到客户端。
在每个 Server Action 中验证输入并检查授权。 Action 是可通过其 ID 访问的公共 HTTP 端点,因此在 Action 验证之前,调用者的身份和负载结构都未经验证。
在变更后、任何 redirect() 之前调用 revalidatePath() 或 revalidateTag()。 下一次渲染会读取缓存,而 redirect() 会抛出异常,因此它之后的代码不会运行。
对 params、searchParams、cookies() 和 headers() 使用 await。 从 15 起,这四者都是 Promise。
</critical_requirements>
自动检测: page.tsx、layout.tsx、loading.tsx、error.tsx、not-found.tsx、default.tsx、 template.tsx、"use client"、"use server"、"use cache"、App Router、Server Components、Client Components、generateMetadata、generateStaticParams、parallel routes、intercepting routes、 revalidatePath、revalidateTag、updateTag、useActionState、useFormStatus、useOptimistic、next/form、 next/cache、next/navigation、cacheComponents、cacheLife、cacheTag、PPR、Turbopack、typedRoutes、 instrumentation.ts、proxy.ts、after()
适用范围:
- 通过
app/目录为应用设置路由,包括布局、加载状态和错误边界 - 决定哪些组件在服务器上运行,哪些会发送到浏览器
- 使用 Suspense 实现流式传输和渐进式渲染
- 通过 Metadata API 设置 SEO,并使用
generateStaticParams进行静态生成 - 并行路由和拦截路由,包括带可分享 URL 的模态框
- 在 Server Actions 中创建、更新和删除数据,并在之后使缓存失效
其他部分负责:
- 持久化 — Action 会调用数据层;此处不确定客户端或查询结构
- 身份验证和会话检索 — Action 会询问调用者的身份,并根据答案采取操作
- 架构验证 — Action 会通过架构解析
FormData;使用哪种架构库是另一个独立选择 - 组件样式 — 组件接收
className,样式方案由其他部分负责 - 生命周期超出单个组件树的客户端状态 — Client Components 通过 props 接收数据
## 理念
App Router 颠覆了 React 的默认行为:组件默认在服务端运行,除非明确指定;客户端 JavaScript 则需要显式启用。这样一来,客户端与服务端之间的请求瀑布便不复存在,因为组件可以等待自己的数据;同时,打包体积也与页面实际所需的交互性相称。
以下四个结果塑造了这里的每种模式:
- 流式传输 — HTML 会在准备就绪时传到浏览器,而不是等最慢的查询完成后才传输
- 就近放置 — 数据获取、元数据和 UI 位于同一段中
- 嵌套布局 — 布局会在其所属段内的导航过程中持续存在,因此其状态和 DOM 都会保留
- 服务端变更 — Server Action 在服务端运行,并在同一轮往返中重新验证缓存,因此 UI 和数据会一同刷新
Server Action 适用于表单提交以及与 UI 绑定的变更。Route Handler 适用于外部 API 调用方、Webhook,以及必须并行运行的任务——从客户端调用的 Action 会排队执行。
useActionState、useFormStatus 和 useOptimistic 是 React 19 的 Hook,而非 Next.js API;useActionState 取代了 Canary 时代的 ReactDOM.useFormState。
核心模式
模式 1:服务端/客户端边界
在 Server Component 中获取数据并向下传递;Client Component 只负责交互。
// app/dashboard/page.tsx — a Server Component, no directive needed
export default async function DashboardPage() {
const user = await getUser(); // runs on the server, ships no JavaScript
return <UserProfile user={user} />; // Client Component, receives data as props
}
只要 Server Component 是通过 children 传入,而不是在 Client Component 内部导入,Client Component 仍然可以包裹 Server Component。
完整代码:examples/server-components.md
模式 2:使用 loading.tsx 和 Suspense 进行流式传输
loading.tsx 会覆盖整个路由;为每个区块单独设置 <Suspense>,即可让它们各自就绪后分别显示。
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart /> {/* slow query — streams in independently */}
</Suspense>
<Suspense fallback={<InvoicesSkeleton />}>
<LatestInvoices />
</Suspense>
完整代码:examples/server-components.md
模式 3:路由文件约定
文件夹定义路由;特殊文件则定义该段及其下所有内容的行为。
app/dashboard/
├── layout.tsx persists across navigations within the segment
├── page.tsx the route itself — without it the URL is not reachable
├── loading.tsx wraps page.tsx in a Suspense boundary automatically
├── error.tsx "use client"; receives { error, reset }
└── not-found.tsx rendered when the segment calls notFound()
完整代码:examples/core.md。完整的文件表见 reference.md。
模式 4:动态段
方括号的层级决定匹配一个段、多个段,还是可选地不匹配任何段。
// app/users/[id]/page.tsx → /users/1 params: { id: string }
// app/docs/[...slug]/page.tsx → /docs/a/b params: { slug: string[] }
// app/shop/[[...slug]]/page.tsx → /shop, /shop/a params: { slug?: string[] }
export default async function UserPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
}
配合 generateStaticParams,可在构建时预渲染已知值。
完整代码:examples/core.md
模式 5:路由组、并行路由和拦截路由
三种文件夹约定,可在不改变 URL 的情况下改变布局和渲染方式。
app/
├── (marketing)/layout.tsx route group — one layout per section, no URL segment
├── dashboard/@analytics/ parallel slot — arrives as a prop on dashboard/layout.tsx
├── @modal/(.)photo/[id]/ intercepts /photo/[id] on soft navigation only
└── @modal/default.tsx what a slot renders when nothing matches
每个插槽都需要一个 default.tsx;在 Next.js 16 中,缺少该文件会导致构建失败。
完整代码:examples/route-groups.md 和 examples/parallel-routes.md
模式 6:元数据
已知值时导出静态对象;值来自路由自身数据时则导出函数。
export const metadata: Metadata = {
title: { template: "%s | Acme", default: "Acme" },
};
// or, when the title depends on the route's data:
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
const post = await getPost((await params).slug); // memoised with the page's own fetch
return {
title: post.title,
openGraph: { title: post.title, type: "article" },
};
}
完整代码:examples/metadata.md
模式 7:定义并调用 Server Action
文件级的 'use server' 会将每个导出标记为 Action,因此 Client Component 可以导入它。
// app/actions.ts
"use server";
export async function createPost(formData: FormData) {
/* ... */
}
// app/posts/new/page.tsx — a plain form, so it submits without JavaScript
<form action={createPost}>...</form>;
action.bind(null, id) 可以传递 FormData 之外的参数,并让表单在未增强的情况下继续正常工作。对于非表单触发的操作,请在 startTransition 中调用 Action,避免点击操作阻塞 UI。
完整代码:examples/server-actions.md 和 examples/event-handlers.md
模式 8:在 Action 中进行验证和授权
这两项检查都应放在 Action 函数体中,因为这是攻击者无法绕过的唯一代码路径。
"use server";
export async function deletePost(postId: string) {
const user = await getCurrentUser();
if (!user) throw new Error("Unauthorized");
const post = await getPost(postId);
if (post?.authorId !== user.id) throw new Error("Forbidden");
await deletePostRecord(postId);
revalidatePath("/posts");
}
请返回验证失败结果,而不要抛出异常——抛出异常会触发错误边界并清空表单。
完整代码:examples/server-actions.md
### 模式 9:重新验证,然后重定向revalidatePath 针对已知路由;revalidateTag 针对数据,无论数据显示在哪里。
"use server";
export async function createPost(formData: FormData) {
// ...mutate
revalidatePath("/posts"); // v16: revalidateTag("posts", "max")
redirect("/posts"); // throws — nothing below this line runs
}
模式 10:待处理状态与乐观状态
三个 React 19 钩子涵盖等待场景:一个用于表单状态,一个用于嵌套的提交按钮,一个用于 在结果返回前展示结果。
const [state, formAction, isPending] = useActionState(createPost, initialState);
// useFormStatus() reads the same pending flag, but only from a component *inside* the form
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(state, content: string) => [
...state,
{ id: `temp-${Date.now()}`, content, pending: true },
],
);
完整代码:examples/optimistic.md 和 examples/server-actions.md
<red_flags>
风险信号
会在运行时出错:
- 操作文件中缺少
'use server'—— 函数会被打包到客户端,其导入项以及这些导入项能够访问到的任何机密也会一并打包 - 操作中没有授权检查 —— 操作 ID 可被发现,因此任何人都能调用它
- 操作中没有输入验证 ——
FormData会以未经解析且无类型的形式,从未经身份验证的调用方传入 - 在
try/catch中调用redirect()—— 它通过抛出异常来发出信号,因此catch会吞掉导航 - 读取
params、searchParams、cookies()或headers()时未使用await—— 从 15 起,这四者都是 Promise error.tsx缺少"use client"——reset回调需要客户端边界- 在
"use client"模块中读取机密 —— 该值会出现在浏览器包中 - 在 Next.js 16 中:并行路由插槽缺少
default.tsx—— 构建会失败 - 在 Next.js 16 中:只传一个参数调用
revalidateTag("tag")—— 现在必须提供cacheLife配置文件
令人意外的行为:
- 在渲染表单的组件中调用
useFormStatus(),pending: false会一直不变;它只会读取其所在的嵌套表单 - 变更后未调用
revalidatePath/revalidateTag,会导致 UI 继续显示变更前的缓存内容,且不会显示任何错误 - 从客户端并行调用的 Server Actions 会排队,并逐个运行
- 验证失败时抛出异常会清空用户填写的表单——应改为返回错误
- 在
page.tsx上使用"use client"会使整个子树都在客户端运行。这虽然合法,却会悄然舍弃所有服务器端渲染的优势 - 从 15 起,在动态渲染下,
fetch默认不会缓存,GET Route Handler 也不会缓存 - 在操作中设置或删除 Cookie 会触发服务器重新渲染
更多注意事项、完整的反模式列表以及各版本的变更,请参阅 reference.md。
</red_flags>