mcpField
W
web-meta-framework-nextjs

web-meta-framework-nextjs

github.com/agents-inc开发2686 次安装2 天前更新
代码仓库

关于 web-meta-framework-nextjs

面向使用 Next.js App Router 的开发者,提供路由、组件边界、缓存和 Server Actions 的实现模式。内容涵盖版本差异与常见陷阱,不适用于 Pages Router。

主要功能

  1. 01Server 与 Client 组件边界
  2. 02路由、布局与动态段模式
  3. 03Server Actions 与数据变更
  4. 04缓存失效与重新验证
  5. 05流式渲染及元数据设置

适用场景

  1. 01规划 app/ 目录路由与布局
  2. 02选择组件在服务器或浏览器运行
  3. 03实现表单操作并刷新缓存
  4. 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.yaml
reference.md
examples/
cookies.md
core.md
event-handlers.md
metadata.md
mutations.md
optimistic.md
parallel-routes.md
revalidation.md
route-groups.md
server-actions.md
server-components.md
streaming.md
versions.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。

详细资源:


适用路径

  • 使用 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 则需要显式启用。这样一来,客户端与服务端之间的请求瀑布便不复存在,因为组件可以等待自己的数据;同时,打包体积也与页面实际所需的交互性相称。

以下四个结果塑造了这里的每种模式:

  1. 流式传输 — HTML 会在准备就绪时传到浏览器,而不是等最慢的查询完成后才传输
  2. 就近放置 — 数据获取、元数据和 UI 位于同一段中
  3. 嵌套布局 — 布局会在其所属段内的导航过程中持续存在,因此其状态和 DOM 都会保留
  4. 服务端变更 — 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
}

完整代码:examples/revalidation.md

模式 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>

评论

支持 Markdown