在 Next.js 14 中,路由系统是最核心的功能之一。App Router 不仅带来了更好的性能,还提供了更灵活的路由组织方式。今天,我们就来深入探讨 Next.js 14 的路由系统。

路由组织结构

1. 基础约定

Next.js 14 的路由基于文件系统,每个文件夹代表一个路由段:

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. 路由组织最佳实践

// 使用路由组进行功能分组
app/
├── (auth)/              # 认证相关路由
│   ├── login/
│   │   └── page.tsx
│   └── register/
       └── page.tsx
├── (dashboard)/         # 仪表板相关路由
│   ├── layout.tsx      # 仪表板共享布局
│   ├── overview/
│   │   └── page.tsx
│   └── settings/
│       └── page.tsx
└── (public)/           # 公开页面
    ├── layout.tsx
    └── page.tsx

动态路由设计

1. 单一动态段

// 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. 多段动态路由

// 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. 捕获所有路由

// 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. 路由中间件

// 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. 路由拦截

// 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. 平行路由

// 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. 路由拦截模式

// 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. 加载界面

// 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. 流式渲染

// 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. 路由段缓存

// 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. 按需重新验证

// 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. 条件路由

// 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. 路由组合

// 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>
  );
}

性能优化建议

  1. 路由预加载

    import { useRouter } from 'next/navigation';
    
    function NavLink({ href, children }) {
      const router = useRouter();
      
      return (
     <a
       href={href}
       onMouseEnter={() => router.prefetch(href)}
     >
       {children}
     </a>
      );
    }
  2. 选择性缓存

    async function getData() {
      const res = await fetch('https://api.example.com/data', {
     next: {
       tags: ['data'],      // 使用标签进行缓存
       revalidate: 3600,    // 缓存时间
     },
      });
      
      return res.json();
    }

写在最后

Next.js 14 的路由系统提供了强大的功能和灵活的配置选项。合理使用这些特性,可以构建出性能优秀、用户体验良好的应用。记住以下几点:

  1. 合理组织路由结构,使用路由组进行功能分组
  2. 善用动态路由和路由拦截
  3. 适当配置缓存策略
  4. 注意性能优化
  5. 使用中间件增强路由功能

在下一篇文章中,我们将深入探讨 Next.js 14 的数据获取和状态管理。如果你有任何问题或建议,欢迎在评论区讨论!

如果觉得这篇文章对你有帮助,别忘了点个赞 👍


远洋录
3 声望0 粉丝

🚀 独立开发者 | 技术出海实践者