在 Next.js 14 中,路由系统是最核心的功能之一。App Router 不仅带来了更好的性能,还提供了更灵活的路由组织方式。今天,我们就来深入探讨 Next.js 14 的路由系统。
路由组织结构
1. 基础约定
Next.js 14 的路由基于文件系统,每个文件夹代表一个路由段:
ini
app/
├── page.tsx # 首页 (/)
├── about/
│ └── page.tsx # 关于页面 (/about)
├── blog/
│ ├── page.tsx # 博客列表页 (/blog)
│ └── [slug]/ # 动态路由
│ └── page.tsx # 博客详情页 (/blog/post-1)
└── (marketing)/ # 路由组
├── pricing/
│ └── page.tsx # 价格页面 (/pricing)
└── contact/
└── page.tsx # 联系页面 (/contact)
2. 路由组织最佳实践
typescript
// 使用路由组进行功能分组
app/
├── (auth)/ # 认证相关路由
│ ├── login/
│ │ └── page.tsx
│ └── register/
└── page.tsx
├── (dashboard)/ # 仪表板相关路由
│ ├── layout.tsx # 仪表板共享布局
│ ├── overview/
│ │ └── page.tsx
│ └── settings/
│ └── page.tsx
└── (public)/ # 公开页面
├── layout.tsx
└── page.tsx
动态路由设计
1. 单一动态段
typescript
// app/users/[id]/page.tsx
export default async function UserProfile({
params
}: {
params: { id: string }
}) {
const user = await fetchUser(params.id);
return (
<div className="user-profile">
<h1>{user.name}</h1>
<div className="user-details">
{/* 用户详情 */}
</div>
</div>
);
}
// 生成静态路由参数
export async function generateStaticParams() {
const users = await fetchUsers();
return users.map((user) => ({
id: user.id.toString(),
}));
}
2. 多段动态路由
typescript
// app/[category]/[subCategory]/[productId]/page.tsx
interface ProductPageProps {
params: {
category: string;
subCategory: string;
productId: string;
};
}
export default async function ProductPage({ params }: ProductPageProps) {
const { category, subCategory, productId } = params;
const product = await fetchProduct({
category,
subCategory,
productId,
});
return (
<div className="product-page">
<Breadcrumb
items={[category, subCategory, product.name]}
/>
<ProductDetails product={product} />
</div>
);
}
3. 捕获所有路由
typescript
// app/docs/[...slug]/page.tsx
export default async function DocPage({
params
}: {
params: { slug: string[] }
}) {
// slug 是一个数组,包含所有路径段
const path = params.slug.join('/');
const doc = await fetchDoc(path);
return (
<div className="doc-content">
<DocRenderer content={doc.content} />
</div>
);
}
路由拦截与中间件
1. 路由中间件
typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 获取当前路径
const path = request.nextUrl.pathname;
// 检查认证状态
const isAuthenticated = request.cookies.has('auth-token');
// 保护的路由
if (path.startsWith('/dashboard') && !isAuthenticated) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 添加自定义请求头
const response = NextResponse.next();
response.headers.set('x-custom-header', 'my-value');
return response;
}
// 配置中间件匹配路径
export const config = {
matcher: [
'/dashboard/:path*',
'/api/:path*',
],
};
2. 路由拦截
typescript
// app/posts/[id]/page.tsx
import { headers } from 'next/headers';
export default async function Post({ params }: { params: { id: string } }) {
const headersList = headers();
const referer = headersList.get('referer');
// 检查是否从允许的来源访问
if (referer && !isAllowedReferer(referer)) {
redirect('/unauthorized');
}
const post = await fetchPost(params.id);
return <PostContent post={post} />;
}
平行路由和拦截路由
1. 平行路由
typescript
// app/layout.tsx
export default function Layout({
children,
auth,
modal,
}: {
children: React.ReactNode;
auth: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<div className="layout">
{children}
{auth}
{modal}
</div>
);
}
// app/@auth/login/page.tsx
export default function LoginModal() {
return (
<div className="modal">
<LoginForm />
</div>
);
}
// app/@modal/photo/[id]/page.tsx
export default function PhotoModal({
params,
}: {
params: { id: string };
}) {
return (
<div className="modal">
<PhotoViewer id={params.id} />
</div>
);
}
2. 路由拦截模式
typescript
// app/feed/page.tsx
import { Feed } from '@/components/Feed';
export default function FeedPage() {
return <Feed />;
}
// app/feed/(..)photo/[id]/page.tsx
export default function InterceptedPhotoModal({
params,
}: {
params: { id: string };
}) {
return (
<div className="modal">
<PhotoViewer id={params.id} />
</div>
);
}
路由加载状态
1. 加载界面
typescript
// app/posts/loading.tsx
export default function Loading() {
return (
<div className="loading-container">
<div className="loading-skeleton">
<div className="skeleton-header" />
<div className="skeleton-content" />
</div>
</div>
);
}
2. 流式渲染
typescript
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div className="dashboard">
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<ChartsSkeleton />}>
<Charts />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<Table />
</Suspense>
</div>
);
}
路由缓存策略
1. 路由段缓存
typescript
// app/products/[id]/page.tsx
export const revalidate = 3600; // 1小时后重新验证
export default async function ProductPage({
params,
}: {
params: { id: string };
}) {
const product = await fetchProduct(params.id);
return (
<div className="product">
<h1>{product.name}</h1>
<ProductDetails product={product} />
</div>
);
}
2. 按需重新验证
typescript
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const { path, tag } = await request.json();
if (path) {
revalidatePath(path);
}
if (tag) {
revalidateTag(tag);
}
return Response.json({ revalidated: true });
}
高级路由技巧
1. 条件路由
typescript
// app/[lang]/layout.tsx
export default async function LocaleLayout({
children,
params: { lang },
}: {
children: React.ReactNode;
params: { lang: string };
}) {
const messages = await loadMessages(lang);
return (
<html lang={lang}>
<body>
<LocaleProvider messages={messages}>
{children}
</LocaleProvider>
</body>
</html>
);
}
// 生成静态路由参数
export async function generateStaticParams() {
return [
{ lang: 'en' },
{ lang: 'zh' },
{ lang: 'ja' },
];
}
2. 路由组合
typescript
// app/(shop)/products/[category]/layout.tsx
export default function ShopLayout({
children,
params,
}: {
children: React.ReactNode;
params: { category: string };
}) {
return (
<div className="shop-layout">
<aside>
<CategoryNav category={params.category} />
</aside>
<main>{children}</main>
</div>
);
}
性能优化建议
- 路由预加载:
typescript
import { useRouter } from 'next/navigation';
function NavLink({ href, children }) {
const router = useRouter();
return (
<a
href={href}
onMouseEnter={() => router.prefetch(href)}
>
{children}
</a>
);
}
- 选择性缓存:
typescript
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: {
tags: ['data'], // 使用标签进行缓存
revalidate: 3600, // 缓存时间
},
});
return res.json();
}
写在最后
Next.js 14 的路由系统提供了强大的功能和灵活的配置选项。合理使用这些特性,可以构建出性能优秀、用户体验良好的应用。记住以下几点:
- 合理组织路由结构,使用路由组进行功能分组
- 善用动态路由和路由拦截
- 适当配置缓存策略
- 注意性能优化
- 使用中间件增强路由功能
在下一篇文章中,我们将深入探讨 Next.js 14 的数据获取和状态管理。如果你有任何问题或建议,欢迎在评论区讨论!
如果觉得这篇文章对你有帮助,别忘了点个赞 👍