commit c7776c6cfc591867e4c73df355991a7254cd026f Author: wehub-skill-sync Date: Mon Jul 13 21:35:53 2026 +0800 chore: import zh skill nextjs-advanced-routing diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..be2e649 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,9 @@ +# WeHub 来源说明 + +- Skill 名称:`nextjs-advanced-routing` +- 中文类目:Next.js App Router 前端界面与架构实现 +- 上游仓库:`wsimmonds__claude-nextjs-skills` +- 上游路径:`nextjs-advanced-routing/SKILL.md` +- 上游链接:https://github.com/wsimmonds/claude-nextjs-skills/blob/HEAD/nextjs-advanced-routing/SKILL.md +- 本仓库为 WeHub 中文 Skill 汉化包,基于 skill 市场筛选 Top200 清单整理 +- 原作者、版权和许可证信息以上游仓库为准 diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..4bddbc1 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,1478 @@ +--- +name: nextjs-advanced-routing +description: 高级 Next.js App Router 模式指南,涵盖路由处理器、并行路由、拦截路由、服务端操作、错误边界、草稿模式以及基于 Suspense 的流式渲染。**关键内容**:服务端操作(action.ts、actions.ts 文件、'use server' 指令)、从客户端组件设置 Cookie 以及表单处理。适用于涉及服务端操作、表单提交、Cookie、数据变更、API 路由、`route.ts`、并行路由、拦截路由或流式渲染的场景。对于将服务端操作与客户端组件分离至关重要。 +allowed-tools: Read, Write, Edit, Glob, Grep, Bash +--- + +# Next.js 高级路由 + +## 概述 + +提供 Next.js App Router 高级特性的全面指南,包括路由处理器(API 路由)、并行路由、拦截路由、服务端操作、错误处理、草稿模式以及基于 Suspense 的流式渲染。 + +## TypeScript:严禁使用 `any` 类型 + +**关键规则:** 本代码库已启用 `@typescript-eslint/no-explicit-any`。使用 `any` 将导致构建失败。 + +**❌ 错误用法:** +```typescript +function handleSubmit(e: any) { ... } +const data: any[] = []; +``` + +**✅ 正确用法:** +```typescript +function handleSubmit(e: React.FormEvent) { ... } +const data: string[] = []; +``` + +### 常见 Next.js 类型模式 + +```typescript +// 页面属性 +function Page({ params }: { params: { slug: string } }) { ... } +function Page({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) { ... } + +// 表单事件 +const handleSubmit = (e: React.FormEvent) => { ... } +const handleChange = (e: React.ChangeEvent) => { ... } + +// 服务端操作 +async function myAction(formData: FormData) { ... } +``` + +## 何时使用本技能 + +在以下场景使用本技能: +- 使用路由处理器创建 API 端点 +- 实现并行或拦截路由 +- 使用服务端操作构建表单 +- 设置 Cookie 或处理数据变更 +- 创建错误边界 +- 实现 CMS 预览的草稿模式 +- 设置流式渲染和 Suspense 边界 +- 构建复杂路由模式(模态框、抽屉) + +## ⚠️ 关键:服务端操作文件的命名与位置 + +当需求中指定了特定文件名时,请严格遵循该指示。如果未指定名称,请选择最符合项目约定的选项——`app/actions.ts` 是多个操作集合的安全默认选择,而 `app/action.ts` 适用于单个表单处理器。 + +### 在 `action.ts` 与 `actions.ts` 之间选择 + +- **匹配现有模式:** 检查项目中是否已存在操作文件,若合适则扩展它。 +- **单个导出 vs 多个导出:** 单个操作优先使用 `action.ts`,一组相关操作优先使用 `actions.ts`。 +- **明确要求:** 如果需求方指定了具体名称,请不要更改。 + +**位置指南** +- 服务端操作应放在 `app/` 目录下,以便它们能参与 App Router 树。 +- 将文件放在调用它的 UI 旁边,除非它在多个路由之间共享。 +- 除非操作是从多个不同路由触发且保持仅为服务端工具,否则避免将其放在 `lib/` 或 `utils/` 中。 + +**示例放置方式** +``` +app/ +├── actions.ts ← 支持多个路由的共享操作 +└── dashboard/ + └── action.ts ← 与单个页面共置的路由特定操作 +``` + +### 示例:创建 action.ts + +```typescript +// app/action.ts(单个操作示例) +'use server'; + +export async function submitForm(formData: FormData) { + const name = formData.get('name') as string; + // 处理表单 + console.log('Submitted:', name); +} +``` + +### 示例:创建 actions.ts + +```typescript +// app/actions.ts(多个相关操作) +'use server'; + +export async function createPost(formData: FormData) { + // ... +} + +export async function deletePost(id: string) { + // ... +} +``` + +**请记住:** 当项目需求中明确写出了精确文件名时,请严格按此执行。 + +## ⚠️ 关键:服务端操作返回类型——表单操作必须返回 void + +**这是 TypeScript 的要求,不可省略。即使你看到有代码从表单操作中返回数据,那段代码也是错误的。** + +当使用表单操作属性时:`
` +- 该函数**不能有 return 语句**(隐式返回 void) +- TypeScript 会**拒绝任何返回值**,即使是 `return undefined` 或 `return null` +- **重要提示:** 如果你在代码库中看到从表单操作返回数据的示例代码,请忽略它——这是一种反模式。通过删除 return 语句来修复它。 + +❌ 错误用法(导致构建错误): +```typescript +export async function saveForm(formData: FormData) { + 'use server'; + + const name = formData.get('name') as string; + if (!name) throw new Error('Name required'); + + await db.save(name); + return { success: true }; // ❌ 构建错误:类型不匹配 +} + +// 在组件中: + {/* ❌ 期望一个 void 函数 */} + +
+``` + +✅ 正确做法——选项 1(简单表单操作,无返回值): +```typescript +export async function saveForm(formData: FormData) { + 'use server'; + + const name = formData.get('name') as string; + + // 验证——抛出错误而非返回它们 + if (!name) throw new Error('Name required'); + + await db.save(name); + revalidatePath('/'); // 触发 UI 更新 + // 无 return 语句——隐式返回 void +} + +// 在组件中: +
+ + +
+``` + +✅ 正确做法——选项 2(配合 useActionState 获取反馈): +```typescript +export async function saveForm(prevState: any, formData: FormData) { + 'use server'; + + const name = formData.get('name') as string; + if (!name) return { error: 'Name required' }; + + await db.save(name); + return { success: true, message: 'Saved!' }; // ✅ 配合 useActionState 可用 +} + +// 在组件中: +'use client'; +const [state, action] = useActionState(saveForm, null); + +return ( +
+ + + {state?.error &&

{state.error}

} + {state?.success &&

{state.message}

} +
+); +``` + +**关键规则:** `
` 期望返回 `void`。如果需要返回数据,请使用 `useActionState`。 + +## 路由处理器(API 路由) + +### 基本路由处理器 + +路由处理器替代了 Pages Router 中的 API 路由。在 `route.ts` 或 `route.js` 文件中创建它们。 + +```typescript +// app/api/hello/route.ts +export async function GET(request: Request) { + return Response.json({ message: 'Hello World' }); +} + +export async function POST(request: Request) { + const body = await request.json(); + + return Response.json({ + message: 'Data received', + data: body + }); +} +``` + +### 支持的 HTTP 方法 + +```typescript +// app/api/items/route.ts +export async function GET(request: Request) { } +export async function POST(request: Request) { } +export async function PUT(request: Request) { } +export async function PATCH(request: Request) { } +export async function DELETE(request: Request) { } +export async function HEAD(request: Request) { } +export async function OPTIONS(request: Request) { } +``` + +### 动态路由处理器 + +```typescript +// app/api/posts/[id]/route.ts +export async function GET( + request: Request, + { params }: { params: { id: string } } +) { + const id = params.id; + const post = await db.posts.findUnique({ where: { id } }); + + return Response.json(post); +} + +export async function DELETE( + request: Request, + { params }: { params: { id: string } } +) { + await db.posts.delete({ where: { id: params.id } }); + + return Response.json({ success: true }); +} +``` + +### 请求头部与 Cookie + +```typescript +// app/api/profile/route.ts +import { cookies, headers } from 'next/headers'; + +export async function GET(request: Request) { + // 访问请求头部 + const headersList = await headers(); + const authorization = headersList.get('authorization'); + + // 访问 Cookie + const cookieStore = await cookies(); + const sessionToken = cookieStore.get('session-token'); + + if (!sessionToken) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const user = await fetchUser(sessionToken.value); + + return Response.json(user); +} +``` + +### 在路由处理器中设置 Cookie + +```typescript +// app/api/login/route.ts +import { cookies } from 'next/headers'; + +export async function POST(request: Request) { + const { email, password } = await request.json(); + + const token = await authenticate(email, password); + + if (!token) { + return Response.json({ error: 'Invalid credentials' }, { status: 401 }); + } + + // 设置 Cookie + const cookieStore = await cookies(); + cookieStore.set('session-token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24 * 7, // 1 周 + path: '/', + }); + + return Response.json({ success: true }); +} +``` + +### CORS 配置 + +```typescript +// app/api/public/route.ts +export async function GET(request: Request) { + const data = await fetchData(); + + return Response.json(data, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); +} + +export async function OPTIONS(request: Request) { + return new Response(null, { + status: 204, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); +} +``` + +### 流式响应 + +```typescript +// app/api/stream/route.ts +export async function GET() { + const encoder = new TextEncoder(); + + const stream = new ReadableStream({ + async start(controller) { + for (let i = 0; i < 10; i++) { + controller.enqueue(encoder.encode(`data: ${i}\n\n`)); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + controller.close(); + }, + }); + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + }); +} +``` + +## 服务端操作 + +服务端操作允许在不创建 API 端点的情况下执行服务端数据变更。 + +### 基本服务端操作(无返回值) + +```typescript +// app/actions.ts +'use server'; + +import { revalidatePath } from 'next/cache'; + +export async function createPost(formData: FormData) { + const title = formData.get('title') as string; + const content = formData.get('content') as string; + + const post = await db.posts.create({ + data: { title, content }, + }); + + revalidatePath('/posts'); + // 无 return 语句——带有表单的服务端操作应返回 void +} +``` + +**注意:** 有关返回数据与不返回数据的模式对比,请参见下面的"在表单中使用服务端操作"章节。 + +### ⚠️ 关键:服务端操作文件组织 + +**文件命名精确性:** +- 当需求方指定了文件名(例如"在一个名为 `action.ts` 的文件中创建一个服务端操作"),请严格按此执行。 +- 常见文件名:`action.ts`(单数)或 `actions.ts`(复数)——选择与需求说明或现有代码匹配的那一个。 +- 将文件放在适当的目录中:通常是 `app/action.ts` 或 `app/actions.ts`。 + +**'use server' 指令的两种模式:** + +**模式 1:文件级(推荐用于多个操作):** +```typescript +// app/actions.ts +'use server'; // 在文件顶部——所有导出都是服务端操作 + +export async function createPost(formData: FormData) { ... } +export async function updatePost(formData: FormData) { ... } +export async function deletePost(postId: string) { ... } +``` + +**模式 2:函数级(适用于单个操作或混合文件):** +```typescript +// app/action.ts 或任何文件 +export async function createPost(formData: FormData) { + 'use server'; // 在函数内部——只有此函数是服务端操作 + + const title = formData.get('title') as string; + await db.posts.create({ data: { title } }); +} +``` + +**客户端组件调用服务端操作:** + +当客户端组件需要调用服务端操作时(例如 onClick、表单提交): +1. 在单独的文件中创建带有 'use server' 指令的服务端操作 +2. 在客户端组件中导入并使用它 + +**✅ 正确模式:** +```typescript +// app/actions.ts - 服务端操作文件 +'use server'; + +import { cookies } from 'next/headers'; + +export async function updateUserPreference(key: string, value: string) { + const cookieStore = await cookies(); + cookieStore.set(key, value); + + // 或执行其他服务端操作 + await db.userSettings.update({ [key]: value }); +} + +// app/InteractiveButton.tsx - 客户端组件 +'use client'; + +import { updateUserPreference } from './actions'; + +export default function InteractiveButton() { + const handleClick = () => { + updateUserPreference('theme', 'dark'); + }; + + return ( + + ); +} +``` + +**❌ 错误用法——在同一个文件中混用 'use server' 和 'use client':** +```typescript +// app/CookieButton.tsx +'use client'; // 此文件是客户端组件 + +export async function setCookie() { + 'use server'; // 错误!不能在客户端组件文件中包含服务端操作 + // ... +} +``` + +### 在表单中使用服务端操作——两种模式 + +#### 模式 1:简单表单操作(返回 void / 抛出错误) + +**关键:** 当直接使用表单 `action` 属性时,服务端操作**必须返回 void**(无返回值)。不要返回 `{ success: true }` 或任何对象。 + +**验证规则:** 检查所有输入,如果验证失败则抛出错误。不要返回错误对象。 + +⚠️ **重要提示:** 即使你在代码库中看到从表单操作返回 `{ success: true }` 的示例代码,也**不要复制该模式**。那段代码是反模式。始终: +1. 检查/验证输入 +2. 如果验证失败则抛出错误(不要返回错误对象) +3. 处理请求 +4. 不返回任何内容(返回 void) + +表单操作的正确模式: + +```typescript +// app/actions.ts +'use server'; + +export async function createPost(formData: FormData) { + const title = formData.get('title') as string; + const content = formData.get('content') as string; + + // 验证 + if (!title || !content) { + throw new Error('Title and content are required'); + } + + // 保存到数据库 + await db.posts.create({ data: { title, content } }); + + // 重新验证或重定向——无需返回 + revalidatePath('/posts'); +} + +// app/posts/new/page.tsx +import { createPost } from '@/app/actions'; + +export default function NewPost() { + return ( + + +