关于 nextjs-routing-advanced
面向采用 Next.js App Router 的开发者进阶路由参考技能,介绍动态与捕获所有路由、并行和拦截路由、导航及 Route Handler。
主要功能
- 01动态路由与捕获所有路由模式
- 02路由组与私有文件夹
- 03并行路由与拦截路由
- 04编程式导航方法
- 05HTTP Route Handler 模式
适用场景
- 01构建基于 slug 的博客或商品页面
- 02用并行路由组织仪表盘布局
- 03将图库条目作为模态路由打开
- 04使用 Route Handler 创建 API 端点
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ josiahsiegel/ claude-plugin-marketplace --skill nextjs-routing-advanced安装后在 Claude Code 中输入/nextjs-routing-advanced确认已加载。
详细介绍
快速参考
| 路由类型 | 文件夹模式 | URL 示例 |
|---|---|---|
| 动态 | [slug] | /blog/hello → { slug: 'hello' } |
| 全匹配 | [...slug] | /docs/a/b → { slug: ['a', 'b'] } |
| 可选全匹配 | [[...slug]] | /shop 或 /shop/a/b |
| 路由组 | (name) | 不影响 URL,用于布局分组 |
| 并行路由 | @slot | 独立加载/错误处理 |
| 拦截同级路由 | (.)path | 模态框模式 |
| 私有文件夹 | _folder | 不是路由 |
| 导航方式 | 代码 | 使用场景 |
|---|---|---|
| Link | <Link href="/path"> | 声明式导航 |
| router.push | router.push('/path') | 编程式导航 |
| router.replace | router.replace('/path') | 不添加历史记录 |
| redirect | redirect('/path') | 服务端重定向 |
| 路由处理器 | 方法 | 模式 |
|---|---|---|
GET | 读取 | export async function GET() {} |
POST | 创建 | export async function POST() {} |
PUT | 更新 | export async function PUT() {} |
DELETE | 删除 | export async function DELETE() {} |
何时使用此技能
适用于高级路由模式:
- 使用 slug 的动态博客/产品页面
- 使用全匹配路由的文档
- 使用并行路由的仪表盘布局
- 使用拦截路由的照片库模态框
- 使用路由处理器的 API 端点
相关技能:
- App Router 基础:参见
nextjs-app-router - 中间件路由:参见
nextjs-middleware - 路由中的数据:参见
nextjs-data-fetching
Next.js 高级路由
动态路由
单个动态段
// app/blog/[slug]/page.tsx
interface PageProps {
params: Promise<{ slug: string }>;
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}
// /blog/hello-world → { slug: 'hello-world' }
多个动态段
// app/shop/[category]/[product]/page.tsx
interface PageProps {
params: Promise<{ category: string; product: string }>;
}
export default async function ProductPage({ params }: PageProps) {
const { category, product } = await params;
const productData = await getProduct(category, product);
return <div>{productData.name}</div>;
}
// /shop/electronics/laptop → { category: 'electronics', product: 'laptop' }
全匹配段
// app/docs/[...slug]/page.tsx
interface PageProps {
params: Promise<{ slug: string[] }>;
}
export default async function DocsPage({ params }: PageProps) {
const { slug } = await params;
// slug is an array of path segments
const doc = await getDoc(slug.join('/'));
return <div>{doc.content}</div>;
}
// /docs/getting-started → { slug: ['getting-started'] }
// /docs/api/auth/login → { slug: ['api', 'auth', 'login'] }
可选全匹配段
// app/shop/[[...slug]]/page.tsx
interface PageProps {
params: Promise<{ slug?: string[] }>;
}
export default async function ShopPage({ params }: PageProps) {
const { slug } = await params;
if (!slug) {
// /shop - show all products
return <AllProducts />;
}
if (slug.length === 1) {
// /shop/category - show category
return <CategoryProducts category={slug[0]} />;
}
// /shop/category/product - show product
return <ProductDetail category={slug[0]} product={slug[1]} />;
}
// /shop → { slug: undefined }
// /shop/electronics → { slug: ['electronics'] }
// /shop/electronics/laptop → { slug: ['electronics', 'laptop'] }
路由组
组织路由且不影响 URL
app/
├── (marketing)/
│ ├── layout.tsx # Marketing layout
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
│
├── (shop)/
│ ├── layout.tsx # Shop layout
│ ├── products/
│ │ └── page.tsx # /products
│ └── cart/
│ └── page.tsx # /cart
│
└── (auth)/
├── layout.tsx # Auth layout (centered, minimal)
├── login/
│ └── page.tsx # /login
└── register/
└── page.tsx # /register
多个根布局
// app/(marketing)/layout.tsx
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<MarketingHeader />
{children}
<MarketingFooter />
</body>
</html>
);
}
// app/(app)/layout.tsx
export default function AppLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<AppSidebar />
<main>{children}</main>
</body>
</html>
);
}
并行路由
基本并行路由
app/
├── layout.tsx
├── page.tsx
├── @analytics/
│ ├── page.tsx
│ └── loading.tsx
├── @team/
│ ├── page.tsx
│ └── loading.tsx
└── @notifications/
└── page.tsx
// app/layout.tsx
export default function Layout({
children,
analytics,
team,
notifications,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
notifications: React.ReactNode;
}) {
return (
<div className="dashboard">
<main>{children}</main>
<aside>
{analytics}
{team}
{notifications}
</aside>
</div>
);
}
使用并行路由进行条件渲染
// app/layout.tsx
import { auth } from '@/lib/auth';
export default async function Layout({
children,
admin,
user,
}: {
children: React.ReactNode;
admin: React.ReactNode;
user: React.ReactNode;
}) {
const session = await auth();
return (
<div>
{children}
{session?.role === 'admin' ? admin : user}
</div>
);
}
默认插槽
// app/@analytics/default.tsx
// Shown when the slot doesn't match current route
export default function AnalyticsDefault() {
return null; // or a default UI
}
拦截路由
模态框模式
app/
├── feed/
│ └── page.tsx # /feed - main feed
├── photo/
│ └── [id]/
│ └── page.tsx # /photo/123 - full page photo
└── @modal/
└── (.)photo/
└── [id]/
└── page.tsx # Intercepted: shows modal
拦截约定
(.) - Match same level
(..) - Match one level above
(..)(..) - Match two levels above
(...) - Match from root
照片库模态框示例
// app/feed/page.tsx
import Link from 'next/link';
export default function FeedPage() {
const photos = await getPhotos();
return (
<div className="grid">
{photos.map((photo) => (
<Link key={photo.id} href={`/photo/${photo.id}`}>
<img src={photo.thumbnail} alt={photo.title} />
</Link>
))}
</div>
);
}
// app/@modal/(.)photo/[id]/page.tsx
import { Modal } from '@/components/modal';
export default async function PhotoModal({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<Modal>
<img src={photo.url} alt={photo.title} />
<p>{photo.description}</p>
</Modal>
);
}
// app/photo/[id]/page.tsx - Full page view (direct navigation)
export default async function PhotoPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<div className="photo-page">
<img src={photo.url} alt={photo.title} />
<h1>{photo.title}</h1>
<p>{photo.description}</p>
</div>
);
}
// app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<html>
<body>
{children}
{modal}
</body>
</html>
);
}
// components/modal.tsx
'use client';
import { useRouter } from 'next/navigation';
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter();
return (
<div className="modal-overlay" onClick={() => router.back()}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<button onClick={() => router.back()}>Close</button>
{children}
</div>
</div>
);
}
私有文件夹
app/
├── _components/ # Private - not a route
│ ├── Button.tsx
│ └── Card.tsx
├── _lib/ # Private - not a route
│ └── utils.ts
├── dashboard/
│ ├── _components/ # Private - scoped to dashboard
│ │ └── Chart.tsx
│ └── page.tsx
└── page.tsx
路由处理器
HTTP 方法
// app/api/posts/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const posts = await getPosts();
return NextResponse.json(posts);
}
export async function POST(request: Request) {
const body = await request.json();
const post = await createPost(body);
return NextResponse.json(post, { status: 201 });
}
export async function PUT(request: Request) {
const body = await request.json();
const post = await updatePost(body);
return NextResponse.json(post);
}
export async function DELETE(request: Request) {
await deletePost();
return new NextResponse(null, { status: 204 });
}
动态路由处理器
// app/api/posts/[id]/route.ts
interface RouteContext {
params: Promise<{ id: string }>;
}
export async function GET(request: Request, context: RouteContext) {
const { id } = await context.params;
const post = await getPost(id);
if (!post) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(post);
}
路由处理器选项
// Force dynamic
export const dynamic = 'force-dynamic';
// Set runtime
export const runtime = 'edge';
// Set revalidation
export const revalidate = 60;
URL 查询参数
访问搜索参数
// app/search/page.tsx
interface SearchPageProps {
searchParams: Promise<{ q?: string; page?: string; sort?: string }>;
}
export default async function SearchPage({ searchParams }: SearchPageProps) {
const { q, page = '1', sort = 'relevance' } = await searchParams;
const results = await search({
query: q,
page: parseInt(page),
sort,
});
return (
<div>
<h1>Results for: {q}</h1>
<SearchResults results={results} />
</div>
);
}
客户端 URL 更新
'use client';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
export function SearchFilter() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const updateSearch = (key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
params.set(key, value);
router.push(`${pathname}?${params.toString()}`);
};
return (
<select onChange={(e) => updateSearch('sort', e.target.value)}>
<option value="relevance">Relevance</option>
<option value="date">Date</option>
<option value="price">Price</option>
</select>
);
}
编程式导航
useRouter Hook
'use client';
import { useRouter } from 'next/navigation';
export function NavigationExample() {
const router = useRouter();
return (
<div>
<button onClick={() => router.push('/dashboard')}>
Go to Dashboard
</button>
<button onClick={() => router.replace('/login')}>
Replace with Login
</button>
<button onClick={() => router.back()}>
Go Back
</button>
<button onClick={() => router.forward()}>
Go Forward
</button>
<button onClick={() => router.refresh()}>
Refresh
</button>
<button onClick={() => router.prefetch('/about')}>
Prefetch About
</button>
</div>
);
}
redirect() 函数
// In Server Component or Server Action
import { redirect } from 'next/navigation';
export default async function ProtectedPage() {
const session = await getSession();
if (!session) {
redirect('/login');
}
return <div>Protected content</div>;
}
permanentRedirect() 函数
import { permanentRedirect } from 'next/navigation';
export default async function OldPage() {
permanentRedirect('/new-page'); // 308 status
}
最佳实践
| 实践 | 说明 |
|---|---|
| 使用路由组进行组织 | 按功能或布局分组 |
| 实现加载状态 | 为每个段添加 loading.tsx |
| 在仪表盘中使用并行路由 | 独立的加载/错误状态 |
| 使用拦截路由实现模态框 | 为叠加层提供更好的 UX |
| 整理私有文件夹 | 使用 _ 前缀表示非路由文件夹 |
| 为参数添加类型 | params 和 searchParams 使用 Promise<> |
评论
支持 Markdown