Skip to content

动态路由与参数

课程概述

本课程深入探讨 Next.js 15 中的动态路由系统和参数处理。动态路由是构建现代 Web 应用的核心功能,允许我们创建灵活的 URL 结构,处理动态内容,并实现复杂的路由逻辑。

学习目标:

  • 理解 Next.js 动态路由机制
  • 掌握路由参数的获取和使用
  • 学习嵌套动态路由
  • 理解可选捕获段和捕获所有段
  • 掌握路由组和并行路由
  • 学习路由拦截和条件路由
  • 理解路由优先级
  • 掌握静态参数生成

一、动态路由基础

1.1 基础动态路由

在 Next.js 中,使用方括号 [param] 创建动态路由段:

typescript
// app/posts/[id]/page.tsx
interface PageProps {
  params: Promise<{
    id: string
  }>
}

export default async function PostPage({ params }: PageProps) {
  const { id } = await params
  
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold">Post ID: {id}</h1>
    </div>
  )
}

URL 映射示例:

URL匹配的文件params
/posts/1app/posts/[id]/page.tsx{ id: '1' }
/posts/abcapp/posts/[id]/page.tsx{ id: 'abc' }
/posts/hello-worldapp/posts/[id]/page.tsx{ id: 'hello-world' }

1.2 多个动态段

typescript
// app/blog/[category]/[slug]/page.tsx
interface PageProps {
  params: Promise<{
    category: string
    slug: string
  }>
}

export default async function BlogPostPage({ params }: PageProps) {
  const { category, slug } = await params
  
  return (
    <div className="container mx-auto p-4">
      <nav className="mb-4 text-sm text-gray-600">
        <a href="/blog">Blog</a> / 
        <a href={`/blog/${category}`}>{category}</a> / 
        <span>{slug}</span>
      </nav>
      <h1 className="text-4xl font-bold">
        {category} - {slug}
      </h1>
    </div>
  )
}

URL 映射示例:

URLparams
/blog/tech/nextjs-15{ category: 'tech', slug: 'nextjs-15' }
/blog/design/ui-patterns{ category: 'design', slug: 'ui-patterns' }

1.3 动态路由数据获取

typescript
// app/products/[id]/page.tsx
import { notFound } from 'next/navigation'

interface Product {
  id: string
  name: string
  price: number
  description: string
  images: string[]
}

async function getProduct(id: string): Promise<Product | null> {
  try {
    const res = await fetch(`https://api.example.com/products/${id}`, {
      next: { revalidate: 60 }
    })
    
    if (!res.ok) {
      return null
    }
    
    return res.json()
  } catch (error) {
    console.error('Failed to fetch product:', error)
    return null
  }
}

interface PageProps {
  params: Promise<{ id: string }>
}

export default async function ProductPage({ params }: PageProps) {
  const { id } = await params
  const product = await getProduct(id)
  
  if (!product) {
    notFound()
  }
  
  return (
    <div className="container mx-auto p-4">
      <div className="grid md:grid-cols-2 gap-8">
        <div>
          <img
            src={product.images[0]}
            alt={product.name}
            className="w-full rounded-lg"
          />
        </div>
        <div>
          <h1 className="text-4xl font-bold mb-4">{product.name}</h1>
          <p className="text-3xl text-blue-600 mb-6">${product.price}</p>
          <p className="text-gray-600 mb-6">{product.description}</p>
          <button className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
            Add to Cart
          </button>
        </div>
      </div>
    </div>
  )
}

// 生成静态参数
export async function generateStaticParams() {
  const res = await fetch('https://api.example.com/products')
  const products: Product[] = await res.json()
  
  return products.map(product => ({
    id: product.id
  }))
}

// 生成元数据
export async function generateMetadata({ params }: PageProps) {
  const { id } = await params
  const product = await getProduct(id)
  
  if (!product) {
    return {
      title: 'Product Not Found'
    }
  }
  
  return {
    title: product.name,
    description: product.description,
    openGraph: {
      title: product.name,
      description: product.description,
      images: [product.images[0]]
    }
  }
}

1.4 类型安全的参数

typescript
// types/params.ts
export interface PostParams {
  id: string
}

export interface BlogParams {
  category: string
  slug: string
}

export interface UserParams {
  username: string
}

// app/posts/[id]/page.tsx
import type { PostParams } from '@/types/params'

interface PageProps {
  params: Promise<PostParams>
}

export default async function PostPage({ params }: PageProps) {
  const { id } = await params
  
  // TypeScript 知道 id 是 string 类型
  const postId = parseInt(id, 10)
  
  return <div>Post {postId}</div>
}

二、捕获所有段

2.1 基础捕获所有段

使用 [...slug] 捕获所有后续路径段:

typescript
// app/docs/[...slug]/page.tsx
interface PageProps {
  params: Promise<{
    slug: string[]
  }>
}

export default async function DocsPage({ params }: PageProps) {
  const { slug } = await params
  
  return (
    <div className="container mx-auto p-4">
      <nav className="mb-4 text-sm text-gray-600">
        <a href="/docs">Docs</a>
        {slug.map((segment, index) => (
          <span key={index}>
            {' / '}
            <a href={`/docs/${slug.slice(0, index + 1).join('/')}`}>
              {segment}
            </a>
          </span>
        ))}
      </nav>
      <h1 className="text-4xl font-bold">
        {slug.join(' / ')}
      </h1>
    </div>
  )
}

URL 映射示例:

URLslug
/docs/getting-started['getting-started']
/docs/api/authentication['api', 'authentication']
/docs/guides/deployment/vercel['guides', 'deployment', 'vercel']

2.2 可选捕获所有段

使用 [[...slug]] 使捕获段可选:

typescript
// app/shop/[[...slug]]/page.tsx
interface PageProps {
  params: Promise<{
    slug?: string[]
  }>
}

export default async function ShopPage({ params }: PageProps) {
  const { slug } = await params
  
  if (!slug || slug.length === 0) {
    // 匹配 /shop
    return (
      <div className="container mx-auto p-4">
        <h1 className="text-4xl font-bold mb-6">Shop</h1>
        <div className="grid grid-cols-4 gap-4">
          {/* 显示所有分类 */}
        </div>
      </div>
    )
  }
  
  if (slug.length === 1) {
    // 匹配 /shop/electronics
    const category = slug[0]
    return (
      <div className="container mx-auto p-4">
        <h1 className="text-4xl font-bold mb-6">
          Category: {category}
        </h1>
        {/* 显示该分类的产品 */}
      </div>
    )
  }
  
  // 匹配 /shop/electronics/phones
  const [category, subcategory] = slug
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-4xl font-bold mb-6">
        {category} - {subcategory}
      </h1>
      {/* 显示子分类的产品 */}
    </div>
  )
}

URL 映射示例:

URLslug
/shopundefined
/shop/electronics['electronics']
/shop/electronics/phones['electronics', 'phones']
/shop/electronics/phones/iphone['electronics', 'phones', 'iphone']

2.3 文档系统实现

typescript
// app/docs/[[...slug]]/page.tsx
import { notFound } from 'next/navigation'
import { getDocBySlug, getDocNavigation } from '@/lib/docs'
import { MDXRemote } from 'next-mdx-remote/rsc'

interface PageProps {
  params: Promise<{
    slug?: string[]
  }>
}

export default async function DocsPage({ params }: PageProps) {
  const { slug = [] } = await params
  const path = slug.join('/')
  
  const doc = await getDocBySlug(path || 'index')
  
  if (!doc) {
    notFound()
  }
  
  const navigation = await getDocNavigation()
  
  return (
    <div className="container mx-auto p-4">
      <div className="flex gap-8">
        <aside className="w-64 flex-shrink-0">
          <nav className="sticky top-4">
            <h2 className="font-bold mb-4">Documentation</h2>
            <ul className="space-y-2">
              {navigation.map(item => (
                <li key={item.path}>
                  <a
                    href={`/docs/${item.path}`}
                    className={`block px-3 py-2 rounded ${
                      path === item.path
                        ? 'bg-blue-100 text-blue-700'
                        : 'hover:bg-gray-100'
                    }`}
                  >
                    {item.title}
                  </a>
                  {item.children && (
                    <ul className="ml-4 mt-2 space-y-1">
                      {item.children.map(child => (
                        <li key={child.path}>
                          <a
                            href={`/docs/${child.path}`}
                            className={`block px-3 py-1 rounded text-sm ${
                              path === child.path
                                ? 'bg-blue-50 text-blue-600'
                                : 'hover:bg-gray-50'
                            }`}
                          >
                            {child.title}
                          </a>
                        </li>
                      ))}
                    </ul>
                  )}
                </li>
              ))}
            </ul>
          </nav>
        </aside>
        
        <main className="flex-1 max-w-4xl">
          <article className="prose max-w-none">
            <h1>{doc.title}</h1>
            <MDXRemote source={doc.content} />
          </article>
          
          <div className="mt-12 pt-6 border-t flex justify-between">
            {doc.prev && (
              <a
                href={`/docs/${doc.prev.path}`}
                className="flex items-center gap-2 text-blue-600 hover:text-blue-700"
              >
                <span>←</span>
                <div>
                  <div className="text-sm text-gray-600">Previous</div>
                  <div className="font-semibold">{doc.prev.title}</div>
                </div>
              </a>
            )}
            {doc.next && (
              <a
                href={`/docs/${doc.next.path}`}
                className="flex items-center gap-2 text-blue-600 hover:text-blue-700 ml-auto"
              >
                <div className="text-right">
                  <div className="text-sm text-gray-600">Next</div>
                  <div className="font-semibold">{doc.next.title}</div>
                </div>
                <span>→</span>
              </a>
            )}
          </div>
        </main>
      </div>
    </div>
  )
}

export async function generateStaticParams() {
  const docs = await getAllDocs()
  
  return docs.map(doc => ({
    slug: doc.path.split('/')
  }))
}

// lib/docs.ts
import fs from 'fs/promises'
import path from 'path'
import matter from 'gray-matter'

const docsDirectory = path.join(process.cwd(), 'content/docs')

export async function getDocBySlug(slug: string) {
  try {
    const fullPath = path.join(docsDirectory, `${slug}.mdx`)
    const fileContents = await fs.readFile(fullPath, 'utf8')
    const { data, content } = matter(fileContents)
    
    return {
      title: data.title,
      content,
      prev: data.prev,
      next: data.next
    }
  } catch (error) {
    return null
  }
}

export async function getAllDocs() {
  async function readDir(dir: string, prefix = ''): Promise<any[]> {
    const entries = await fs.readdir(dir, { withFileTypes: true })
    const docs = []
    
    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name)
      const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name
      
      if (entry.isDirectory()) {
        const subDocs = await readDir(fullPath, relativePath)
        docs.push(...subDocs)
      } else if (entry.name.endsWith('.mdx')) {
        const slug = relativePath.replace(/\.mdx$/, '')
        docs.push({ path: slug })
      }
    }
    
    return docs
  }
  
  return readDir(docsDirectory)
}

export async function getDocNavigation() {
  // 实现文档导航结构
  return [
    {
      title: 'Getting Started',
      path: 'getting-started',
      children: [
        { title: 'Installation', path: 'getting-started/installation' },
        { title: 'Quick Start', path: 'getting-started/quick-start' }
      ]
    },
    {
      title: 'API Reference',
      path: 'api',
      children: [
        { title: 'Components', path: 'api/components' },
        { title: 'Hooks', path: 'api/hooks' }
      ]
    }
  ]
}

2.4 文件浏览器实现

typescript
// app/files/[[...path]]/page.tsx
import { getFileTree, getFileContent } from '@/lib/files'
import { notFound } from 'next/navigation'

interface PageProps {
  params: Promise<{
    path?: string[]
  }>
}

export default async function FileBrowserPage({ params }: PageProps) {
  const { path = [] } = await params
  const currentPath = path.join('/')
  
  const tree = await getFileTree(currentPath)
  
  if (!tree) {
    notFound()
  }
  
  if (tree.type === 'file') {
    const content = await getFileContent(currentPath)
    
    return (
      <div className="container mx-auto p-4">
        <nav className="mb-4 text-sm text-gray-600">
          <a href="/files">Files</a>
          {path.map((segment, index) => (
            <span key={index}>
              {' / '}
              <a href={`/files/${path.slice(0, index + 1).join('/')}`}>
                {segment}
              </a>
            </span>
          ))}
        </nav>
        
        <div className="border rounded-lg overflow-hidden">
          <div className="bg-gray-100 px-4 py-2 border-b">
            <h1 className="font-semibold">{path[path.length - 1]}</h1>
          </div>
          <pre className="p-4 overflow-auto">
            <code>{content}</code>
          </pre>
        </div>
      </div>
    )
  }
  
  return (
    <div className="container mx-auto p-4">
      <nav className="mb-4 text-sm text-gray-600">
        <a href="/files">Files</a>
        {path.map((segment, index) => (
          <span key={index}>
            {' / '}
            <a href={`/files/${path.slice(0, index + 1).join('/')}`}>
              {segment}
            </a>
          </span>
        ))}
      </nav>
      
      <div className="border rounded-lg overflow-hidden">
        <table className="w-full">
          <thead className="bg-gray-100">
            <tr>
              <th className="px-4 py-2 text-left">Name</th>
              <th className="px-4 py-2 text-left">Type</th>
              <th className="px-4 py-2 text-left">Size</th>
              <th className="px-4 py-2 text-left">Modified</th>
            </tr>
          </thead>
          <tbody>
            {tree.children?.map(item => (
              <tr key={item.name} className="border-t hover:bg-gray-50">
                <td className="px-4 py-2">
                  <a
                    href={`/files/${[...path, item.name].join('/')}`}
                    className="text-blue-600 hover:underline flex items-center gap-2"
                  >
                    <span>{item.type === 'directory' ? '📁' : '📄'}</span>
                    {item.name}
                  </a>
                </td>
                <td className="px-4 py-2 text-gray-600">
                  {item.type === 'directory' ? 'Folder' : 'File'}
                </td>
                <td className="px-4 py-2 text-gray-600">
                  {item.size ? `${(item.size / 1024).toFixed(2)} KB` : '-'}
                </td>
                <td className="px-4 py-2 text-gray-600">
                  {item.modified ? new Date(item.modified).toLocaleDateString() : '-'}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  )
}

三、查询参数 (Search Params)

3.1 基础查询参数

typescript
// app/search/page.tsx
interface PageProps {
  searchParams: Promise<{
    q?: string
    category?: string
    sort?: string
    page?: string
  }>
}

export default async function SearchPage({ searchParams }: PageProps) {
  const params = await searchParams
  const query = params.q || ''
  const category = params.category || 'all'
  const sort = params.sort || 'relevance'
  const page = parseInt(params.page || '1', 10)
  
  // 执行搜索
  const results = await searchProducts({
    query,
    category,
    sort,
    page
  })
  
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">
        Search Results for "{query}"
      </h1>
      
      <div className="flex gap-8">
        <aside className="w-64">
          <div className="mb-6">
            <h3 className="font-semibold mb-2">Category</h3>
            <select
              value={category}
              onChange={(e) => {
                const url = new URL(window.location.href)
                url.searchParams.set('category', e.target.value)
                window.location.href = url.toString()
              }}
              className="w-full p-2 border rounded"
            >
              <option value="all">All Categories</option>
              <option value="electronics">Electronics</option>
              <option value="clothing">Clothing</option>
              <option value="books">Books</option>
            </select>
          </div>
          
          <div className="mb-6">
            <h3 className="font-semibold mb-2">Sort By</h3>
            <select
              value={sort}
              onChange={(e) => {
                const url = new URL(window.location.href)
                url.searchParams.set('sort', e.target.value)
                window.location.href = url.toString()
              }}
              className="w-full p-2 border rounded"
            >
              <option value="relevance">Relevance</option>
              <option value="price-asc">Price: Low to High</option>
              <option value="price-desc">Price: High to Low</option>
              <option value="rating">Rating</option>
            </select>
          </div>
        </aside>
        
        <main className="flex-1">
          <div className="mb-4 text-gray-600">
            Found {results.total} results
          </div>
          
          <div className="grid md:grid-cols-3 gap-4">
            {results.items.map((item: any) => (
              <div key={item.id} className="border rounded p-4">
                <img
                  src={item.image}
                  alt={item.name}
                  className="w-full h-48 object-cover rounded mb-2"
                />
                <h3 className="font-semibold">{item.name}</h3>
                <p className="text-blue-600">${item.price}</p>
              </div>
            ))}
          </div>
          
          {results.totalPages > 1 && (
            <div className="mt-6 flex justify-center gap-2">
              {Array.from({ length: results.totalPages }, (_, i) => i + 1).map(p => (
                <a
                  key={p}
                  href={`?q=${query}&category=${category}&sort=${sort}&page=${p}`}
                  className={`px-4 py-2 border rounded ${
                    p === page
                      ? 'bg-blue-500 text-white'
                      : 'hover:bg-gray-100'
                  }`}
                >
                  {p}
                </a>
              ))}
            </div>
          )}
        </main>
      </div>
    </div>
  )
}

3.2 客户端查询参数操作

typescript
// app/products/page.tsx
'use client'

import { useSearchParams, useRouter, usePathname } from 'next/navigation'
import { useCallback } from 'react'

export default function ProductsPage() {
  const router = useRouter()
  const pathname = usePathname()
  const searchParams = useSearchParams()
  
  const category = searchParams.get('category') || 'all'
  const minPrice = searchParams.get('minPrice') || ''
  const maxPrice = searchParams.get('maxPrice') || ''
  
  const createQueryString = useCallback(
    (name: string, value: string) => {
      const params = new URLSearchParams(searchParams.toString())
      params.set(name, value)
      return params.toString()
    },
    [searchParams]
  )
  
  const handleCategoryChange = (newCategory: string) => {
    router.push(pathname + '?' + createQueryString('category', newCategory))
  }
  
  const handlePriceFilter = () => {
    const params = new URLSearchParams(searchParams.toString())
    if (minPrice) params.set('minPrice', minPrice)
    if (maxPrice) params.set('maxPrice', maxPrice)
    router.push(pathname + '?' + params.toString())
  }
  
  const clearFilters = () => {
    router.push(pathname)
  }
  
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">Products</h1>
      
      <div className="flex gap-8">
        <aside className="w-64">
          <div className="mb-6">
            <h3 className="font-semibold mb-2">Category</h3>
            <div className="space-y-2">
              {['all', 'electronics', 'clothing', 'books'].map(cat => (
                <label key={cat} className="flex items-center gap-2">
                  <input
                    type="radio"
                    checked={category === cat}
                    onChange={() => handleCategoryChange(cat)}
                  />
                  <span className="capitalize">{cat}</span>
                </label>
              ))}
            </div>
          </div>
          
          <div className="mb-6">
            <h3 className="font-semibold mb-2">Price Range</h3>
            <div className="space-y-2">
              <input
                type="number"
                placeholder="Min"
                value={minPrice}
                onChange={(e) => setMinPrice(e.target.value)}
                className="w-full p-2 border rounded"
              />
              <input
                type="number"
                placeholder="Max"
                value={maxPrice}
                onChange={(e) => setMaxPrice(e.target.value)}
                className="w-full p-2 border rounded"
              />
              <button
                onClick={handlePriceFilter}
                className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
              >
                Apply
              </button>
            </div>
          </div>
          
          <button
            onClick={clearFilters}
            className="w-full px-4 py-2 border rounded hover:bg-gray-100"
          >
            Clear Filters
          </button>
        </aside>
        
        <main className="flex-1">
          {/* 产品列表 */}
        </main>
      </div>
    </div>
  )
}

3.3 查询参数验证

typescript
// lib/searchParams.ts
import { z } from 'zod'

export const searchParamsSchema = z.object({
  q: z.string().optional(),
  category: z.enum(['all', 'electronics', 'clothing', 'books']).optional(),
  minPrice: z.coerce.number().min(0).optional(),
  maxPrice: z.coerce.number().min(0).optional(),
  sort: z.enum(['relevance', 'price-asc', 'price-desc', 'rating']).optional(),
  page: z.coerce.number().min(1).optional()
})

export type SearchParams = z.infer<typeof searchParamsSchema>

export function parseSearchParams(params: any): SearchParams {
  const result = searchParamsSchema.safeParse(params)
  
  if (!result.success) {
    console.error('Invalid search params:', result.error)
    return {}
  }
  
  return result.data
}

// app/search/page.tsx
import { parseSearchParams } from '@/lib/searchParams'

interface PageProps {
  searchParams: Promise<any>
}

export default async function SearchPage({ searchParams }: PageProps) {
  const params = parseSearchParams(await searchParams)
  
  // params 现在是类型安全的
  const {
    q = '',
    category = 'all',
    minPrice,
    maxPrice,
    sort = 'relevance',
    page = 1
  } = params
  
  return (
    <div>
      {/* 使用验证后的参数 */}
    </div>
  )
}

3.4 URL 状态管理

typescript
// hooks/useUrlState.ts
'use client'

import { useSearchParams, useRouter, usePathname } from 'next/navigation'
import { useCallback } from 'react'

export function useUrlState<T extends Record<string, any>>(
  defaultValues: T
): [T, (updates: Partial<T>) => void] {
  const router = useRouter()
  const pathname = usePathname()
  const searchParams = useSearchParams()
  
  const state = Object.keys(defaultValues).reduce((acc, key) => {
    const value = searchParams.get(key)
    acc[key] = value !== null ? value : defaultValues[key]
    return acc
  }, {} as T)
  
  const setState = useCallback(
    (updates: Partial<T>) => {
      const params = new URLSearchParams(searchParams.toString())
      
      Object.entries(updates).forEach(([key, value]) => {
        if (value === undefined || value === null || value === '') {
          params.delete(key)
        } else {
          params.set(key, String(value))
        }
      })
      
      router.push(pathname + '?' + params.toString())
    },
    [router, pathname, searchParams]
  )
  
  return [state, setState]
}

// 使用示例
// app/products/ProductFilters.tsx
'use client'

import { useUrlState } from '@/hooks/useUrlState'

export function ProductFilters() {
  const [filters, setFilters] = useUrlState({
    category: 'all',
    minPrice: '',
    maxPrice: '',
    sort: 'relevance'
  })
  
  return (
    <div className="space-y-4">
      <div>
        <label className="block font-semibold mb-2">Category</label>
        <select
          value={filters.category}
          onChange={(e) => setFilters({ category: e.target.value })}
          className="w-full p-2 border rounded"
        >
          <option value="all">All</option>
          <option value="electronics">Electronics</option>
          <option value="clothing">Clothing</option>
        </select>
      </div>
      
      <div>
        <label className="block font-semibold mb-2">Price Range</label>
        <div className="flex gap-2">
          <input
            type="number"
            placeholder="Min"
            value={filters.minPrice}
            onChange={(e) => setFilters({ minPrice: e.target.value })}
            className="flex-1 p-2 border rounded"
          />
          <input
            type="number"
            placeholder="Max"
            value={filters.maxPrice}
            onChange={(e) => setFilters({ maxPrice: e.target.value })}
            className="flex-1 p-2 border rounded"
          />
        </div>
      </div>
      
      <div>
        <label className="block font-semibold mb-2">Sort By</label>
        <select
          value={filters.sort}
          onChange={(e) => setFilters({ sort: e.target.value })}
          className="w-full p-2 border rounded"
        >
          <option value="relevance">Relevance</option>
          <option value="price-asc">Price: Low to High</option>
          <option value="price-desc">Price: High to Low</option>
        </select>
      </div>
      
      <button
        onClick={() => setFilters({
          category: 'all',
          minPrice: '',
          maxPrice: '',
          sort: 'relevance'
        })}
        className="w-full px-4 py-2 border rounded hover:bg-gray-100"
      >
        Clear Filters
      </button>
    </div>
  )
}

四、路由组与并行路由

4.1 路由组

使用 (folder) 创建路由组,不影响 URL 结构:

typescript
// 文件结构:
// app/
//   (marketing)/
//     about/page.tsx
//     contact/page.tsx
//     layout.tsx
//   (shop)/
//     products/page.tsx
//     cart/page.tsx
//     layout.tsx
//   layout.tsx

// app/(marketing)/layout.tsx
export default function MarketingLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <div>
      <nav className="bg-white border-b">
        <div className="container mx-auto px-4 py-4">
          <div className="flex items-center justify-between">
            <a href="/" className="text-2xl font-bold">Brand</a>
            <div className="flex gap-6">
              <a href="/about">About</a>
              <a href="/contact">Contact</a>
            </div>
          </div>
        </div>
      </nav>
      <main>{children}</main>
      <footer className="bg-gray-100 mt-12">
        <div className="container mx-auto px-4 py-8">
          <p className="text-center text-gray-600">
            © 2024 Brand. All rights reserved.
          </p>
        </div>
      </footer>
    </div>
  )
}

// app/(shop)/layout.tsx
export default function ShopLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <div>
      <nav className="bg-blue-600 text-white">
        <div className="container mx-auto px-4 py-4">
          <div className="flex items-center justify-between">
            <a href="/" className="text-2xl font-bold">Shop</a>
            <div className="flex gap-6">
              <a href="/products">Products</a>
              <a href="/cart">Cart (0)</a>
            </div>
          </div>
        </div>
      </nav>
      <main>{children}</main>
    </div>
  )
}

URL 映射:

文件路径URL
app/(marketing)/about/page.tsx/about
app/(marketing)/contact/page.tsx/contact
app/(shop)/products/page.tsx/products
app/(shop)/cart/page.tsx/cart

4.2 并行路由

使用 @folder 创建并行路由槽:

typescript
// 文件结构:
// app/
//   dashboard/
//     @analytics/page.tsx
//     @team/page.tsx
//     @activity/page.tsx
//     layout.tsx
//     page.tsx

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  team,
  activity
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
  activity: React.ReactNode
}) {
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-4xl font-bold mb-6">Dashboard</h1>
      
      <div className="grid grid-cols-2 gap-6 mb-6">
        <div className="border rounded-lg p-4">
          <h2 className="text-2xl font-semibold mb-4">Analytics</h2>
          {analytics}
        </div>
        <div className="border rounded-lg p-4">
          <h2 className="text-2xl font-semibold mb-4">Team</h2>
          {team}
        </div>
      </div>
      
      <div className="border rounded-lg p-4">
        <h2 className="text-2xl font-semibold mb-4">Recent Activity</h2>
        {activity}
      </div>
      
      <div className="mt-6">
        {children}
      </div>
    </div>
  )
}

// app/dashboard/@analytics/page.tsx
async function getAnalytics() {
  const res = await fetch('https://api.example.com/analytics')
  return res.json()
}

export default async function AnalyticsSlot() {
  const data = await getAnalytics()
  
  return (
    <div className="space-y-4">
      <div className="flex justify-between items-center">
        <div>
          <div className="text-sm text-gray-600">Total Users</div>
          <div className="text-3xl font-bold">{data.totalUsers}</div>
        </div>
        <div className="text-green-600">+12%</div>
      </div>
      <div className="flex justify-between items-center">
        <div>
          <div className="text-sm text-gray-600">Revenue</div>
          <div className="text-3xl font-bold">${data.revenue}</div>
        </div>
        <div className="text-green-600">+8%</div>
      </div>
    </div>
  )
}

// app/dashboard/@team/page.tsx
async function getTeam() {
  const res = await fetch('https://api.example.com/team')
  return res.json()
}

export default async function TeamSlot() {
  const team = await getTeam()
  
  return (
    <div className="space-y-2">
      {team.members.map((member: any) => (
        <div key={member.id} className="flex items-center gap-3">
          <img
            src={member.avatar}
            alt={member.name}
            className="w-10 h-10 rounded-full"
          />
          <div>
            <div className="font-semibold">{member.name}</div>
            <div className="text-sm text-gray-600">{member.role}</div>
          </div>
        </div>
      ))}
    </div>
  )
}

// app/dashboard/@activity/page.tsx
async function getActivity() {
  const res = await fetch('https://api.example.com/activity')
  return res.json()
}

export default async function ActivitySlot() {
  const activities = await getActivity()
  
  return (
    <div className="space-y-3">
      {activities.map((activity: any) => (
        <div key={activity.id} className="flex items-start gap-3">
          <div className="w-2 h-2 mt-2 rounded-full bg-blue-500"></div>
          <div className="flex-1">
            <div className="font-semibold">{activity.title}</div>
            <div className="text-sm text-gray-600">{activity.description}</div>
            <div className="text-xs text-gray-500 mt-1">
              {new Date(activity.timestamp).toLocaleString()}
            </div>
          </div>
        </div>
      ))}
    </div>
  )
}

// app/dashboard/page.tsx
export default function DashboardPage() {
  return (
    <div className="text-center text-gray-600">
      Select a section to view details
    </div>
  )
}

4.3 条件路由

typescript
// app/dashboard/layout.tsx
import { getUser } from '@/lib/auth'

export default async function DashboardLayout({
  children,
  admin,
  user
}: {
  children: React.ReactNode
  admin: React.ReactNode
  user: React.ReactNode
}) {
  const currentUser = await getUser()
  
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-4xl font-bold mb-6">Dashboard</h1>
      {currentUser.role === 'admin' ? admin : user}
      {children}
    </div>
  )
}

// app/dashboard/@admin/page.tsx
export default function AdminDashboard() {
  return (
    <div className="grid grid-cols-3 gap-4 mb-6">
      <div className="border rounded p-4">
        <h3 className="font-semibold mb-2">Total Users</h3>
        <p className="text-3xl font-bold">1,234</p>
      </div>
      <div className="border rounded p-4">
        <h3 className="font-semibold mb-2">Active Sessions</h3>
        <p className="text-3xl font-bold">567</p>
      </div>
      <div className="border rounded p-4">
        <h3 className="font-semibold mb-2">System Health</h3>
        <p className="text-3xl font-bold text-green-600">Good</p>
      </div>
    </div>
  )
}

// app/dashboard/@user/page.tsx
export default function UserDashboard() {
  return (
    <div className="border rounded p-6 mb-6">
      <h2 className="text-2xl font-semibold mb-4">Welcome Back!</h2>
      <p className="text-gray-600">
        Here's what's new since your last visit.
      </p>
    </div>
  )
}

4.4 默认槽

typescript
// app/dashboard/@analytics/default.tsx
export default function DefaultAnalytics() {
  return (
    <div className="text-center text-gray-500 py-8">
      Loading analytics...
    </div>
  )
}

// app/dashboard/@team/default.tsx
export default function DefaultTeam() {
  return (
    <div className="text-center text-gray-500 py-8">
      Loading team...
    </div>
  )
}

五、路由拦截

5.1 基础路由拦截

使用 (.) 拦截同级路由:

typescript
// 文件结构:
// app/
//   photos/
//     [id]/
//       page.tsx
//     (.)[id]/
//       page.tsx
//     page.tsx

// app/photos/page.tsx
import Link from 'next/link'

async function getPhotos() {
  const res = await fetch('https://api.example.com/photos')
  return res.json()
}

export default async function PhotosPage() {
  const photos = await getPhotos()
  
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-4xl font-bold mb-6">Photos</h1>
      <div className="grid grid-cols-4 gap-4">
        {photos.map((photo: any) => (
          <Link
            key={photo.id}
            href={`/photos/${photo.id}`}
            className="aspect-square overflow-hidden rounded-lg hover:opacity-80 transition"
          >
            <img
              src={photo.url}
              alt={photo.title}
              className="w-full h-full object-cover"
            />
          </Link>
        ))}
      </div>
    </div>
  )
}

// app/photos/(.)[id]/page.tsx - 模态框视图
'use client'

import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'

interface PageProps {
  params: Promise<{ id: string }>
}

export default function PhotoModal({ params }: PageProps) {
  const router = useRouter()
  const [photo, setPhoto] = useState<any>(null)
  
  useEffect(() => {
    params.then(({ id }) => {
      fetch(`https://api.example.com/photos/${id}`)
        .then(r => r.json())
        .then(setPhoto)
    })
  }, [params])
  
  if (!photo) return null
  
  return (
    <div
      className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
      onClick={() => router.back()}
    >
      <div
        className="max-w-4xl max-h-[90vh] bg-white rounded-lg overflow-hidden"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="p-4 border-b flex justify-between items-center">
          <h2 className="text-xl font-semibold">{photo.title}</h2>
          <button
            onClick={() => router.back()}
            className="text-gray-500 hover:text-gray-700"
          >

          </button>
        </div>
        <img
          src={photo.url}
          alt={photo.title}
          className="w-full"
        />
        <div className="p-4">
          <p className="text-gray-600">{photo.description}</p>
        </div>
      </div>
    </div>
  )
}

// app/photos/[id]/page.tsx - 完整页面视图
async function getPhoto(id: string) {
  const res = await fetch(`https://api.example.com/photos/${id}`)
  return res.json()
}

interface PageProps {
  params: Promise<{ id: string }>
}

export default async function PhotoPage({ params }: PageProps) {
  const { id } = await params
  const photo = await getPhoto(id)
  
  return (
    <div className="container mx-auto p-4 max-w-4xl">
      <a href="/photos" className="text-blue-600 hover:underline mb-4 block">
        ← Back to Photos
      </a>
      <h1 className="text-4xl font-bold mb-6">{photo.title}</h1>
      <img
        src={photo.url}
        alt={photo.title}
        className="w-full rounded-lg mb-6"
      />
      <p className="text-gray-600 text-lg">{photo.description}</p>
    </div>
  )
}

5.2 拦截级别

typescript
// (.) - 同级
// app/feed/(.)[id]/page.tsx 拦截 app/feed/[id]/page.tsx

// (..) - 上一级
// app/(..)photos/[id]/page.tsx 拦截 app/photos/[id]/page.tsx

// (...) - 根级
// app/feed/(...)photos/[id]/page.tsx 拦截 app/photos/[id]/page.tsx

// 示例: 产品快速查看
// 文件结构:
// app/
//   products/
//     [id]/
//       page.tsx
//     page.tsx
//   category/
//     [slug]/
//       (..)(..)/products/[id]/
//         page.tsx
//       page.tsx

// app/category/[slug]/(..)(..)/products/[id]/page.tsx
'use client'

import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'

interface PageProps {
  params: Promise<{ id: string }>
}

export default function QuickView({ params }: PageProps) {
  const router = useRouter()
  const [product, setProduct] = useState<any>(null)
  
  useEffect(() => {
    params.then(({ id }) => {
      fetch(`https://api.example.com/products/${id}`)
        .then(r => r.json())
        .then(setProduct)
    })
  }, [params])
  
  if (!product) return null
  
  return (
    <div
      className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
      onClick={() => router.back()}
    >
      <div
        className="bg-white rounded-lg p-6 max-w-2xl w-full mx-4"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="flex justify-between items-start mb-4">
          <h2 className="text-2xl font-bold">{product.name}</h2>
          <button
            onClick={() => router.back()}
            className="text-gray-500 hover:text-gray-700"
          >

          </button>
        </div>
        
        <div className="grid md:grid-cols-2 gap-6">
          <img
            src={product.image}
            alt={product.name}
            className="w-full rounded-lg"
          />
          <div>
            <p className="text-3xl font-bold text-blue-600 mb-4">
              ${product.price}
            </p>
            <p className="text-gray-600 mb-6">{product.description}</p>
            <button className="w-full px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
              Add to Cart
            </button>
            <a
              href={`/products/${product.id}`}
              className="block text-center mt-4 text-blue-600 hover:underline"
            >
              View Full Details
            </a>
          </div>
        </div>
      </div>
    </div>
  )
}

5.3 登录模态框

typescript
// 文件结构:
// app/
//   login/
//     page.tsx
//   (.)login/
//     page.tsx

// app/(.)login/page.tsx
'use client'

import { useRouter } from 'next/navigation'
import { useState } from 'react'

export default function LoginModal() {
  const router = useRouter()
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    // 处理登录逻辑
    console.log('Login:', { email, password })
    router.back()
  }
  
  return (
    <div
      className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
      onClick={() => router.back()}
    >
      <div
        className="bg-white rounded-lg p-8 max-w-md w-full mx-4"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="flex justify-between items-center mb-6">
          <h2 className="text-2xl font-bold">Login</h2>
          <button
            onClick={() => router.back()}
            className="text-gray-500 hover:text-gray-700"
          >

          </button>
        </div>
        
        <form onSubmit={handleSubmit} className="space-y-4">
          <div>
            <label className="block font-semibold mb-2">Email</label>
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
              className="w-full p-3 border rounded-lg"
            />
          </div>
          
          <div>
            <label className="block font-semibold mb-2">Password</label>
            <input
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
              className="w-full p-3 border rounded-lg"
            />
          </div>
          
          <button
            type="submit"
            className="w-full px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
          >
            Login
          </button>
        </form>
        
        <p className="text-center mt-4 text-sm text-gray-600">
          Don't have an account?{' '}
          <a href="/signup" className="text-blue-600 hover:underline">
            Sign up
          </a>
        </p>
      </div>
    </div>
  )
}

// app/login/page.tsx
export default function LoginPage() {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-100">
      <div className="bg-white rounded-lg p-8 max-w-md w-full">
        <h1 className="text-3xl font-bold mb-6">Login</h1>
        <form className="space-y-4">
          <div>
            <label className="block font-semibold mb-2">Email</label>
            <input
              type="email"
              required
              className="w-full p-3 border rounded-lg"
            />
          </div>
          
          <div>
            <label className="block font-semibold mb-2">Password</label>
            <input
              type="password"
              required
              className="w-full p-3 border rounded-lg"
            />
          </div>
          
          <button
            type="submit"
            className="w-full px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
          >
            Login
          </button>
        </form>
      </div>
    </div>
  )
}

六、路由优先级与匹配

6.1 路由优先级规则

Next.js 按以下优先级匹配路由:

1. 静态路由 (最高优先级)
   /about/page.tsx

2. 动态路由
   /posts/[id]/page.tsx

3. 捕获所有段
   /docs/[...slug]/page.tsx

4. 可选捕获所有段 (最低优先级)
   /shop/[[...slug]]/page.tsx

示例:

typescript
// 文件结构:
// app/
//   blog/
//     page.tsx                    // 匹配 /blog
//     new/page.tsx                // 匹配 /blog/new (优先于 [slug])
//     [slug]/page.tsx             // 匹配 /blog/anything
//     [slug]/edit/page.tsx        // 匹配 /blog/anything/edit
//     [...slug]/page.tsx          // 不会被匹配 (被上面的规则覆盖)

// URL 匹配示例:
// /blog           → app/blog/page.tsx
// /blog/new       → app/blog/new/page.tsx
// /blog/hello     → app/blog/[slug]/page.tsx
// /blog/hello/edit → app/blog/[slug]/edit/page.tsx

6.2 路由冲突处理

typescript
// 错误示例 - 路由冲突
// app/
//   products/
//     [id]/page.tsx
//     [slug]/page.tsx  // 错误! 与 [id] 冲突

// 正确示例 - 使用不同的路径结构
// app/
//   products/
//     id/
//       [id]/page.tsx
//     slug/
//       [slug]/page.tsx

// 或者使用路由组
// app/
//   products/
//     (by-id)/
//       [id]/page.tsx
//     (by-slug)/
//       [slug]/page.tsx

6.3 复杂路由示例

typescript
// 文件结构:
// app/
//   (shop)/
//     products/
//       page.tsx                          // /products
//       new/page.tsx                      // /products/new
//       [id]/page.tsx                     // /products/123
//       [id]/edit/page.tsx                // /products/123/edit
//       [id]/reviews/page.tsx             // /products/123/reviews
//       [id]/reviews/[reviewId]/page.tsx  // /products/123/reviews/456
//     categories/
//       [[...slug]]/page.tsx              // /categories, /categories/electronics, /categories/electronics/phones

// app/(shop)/products/[id]/page.tsx
interface PageProps {
  params: Promise<{ id: string }>
}

export default async function ProductPage({ params }: PageProps) {
  const { id } = await params
  
  return (
    <div className="container mx-auto p-4">
      <nav className="mb-4 text-sm text-gray-600">
        <a href="/products">Products</a> / Product {id}
      </nav>
      <h1 className="text-4xl font-bold">Product {id}</h1>
    </div>
  )
}

// app/(shop)/products/[id]/reviews/[reviewId]/page.tsx
interface PageProps {
  params: Promise<{
    id: string
    reviewId: string
  }>
}

export default async function ReviewPage({ params }: PageProps) {
  const { id, reviewId } = await params
  
  return (
    <div className="container mx-auto p-4">
      <nav className="mb-4 text-sm text-gray-600">
        <a href="/products">Products</a> /
        <a href={`/products/${id}`}>Product {id}</a> /
        <a href={`/products/${id}/reviews`}>Reviews</a> /
        Review {reviewId}
      </nav>
      <h1 className="text-4xl font-bold">
        Review {reviewId} for Product {id}
      </h1>
    </div>
  )
}

七、静态参数生成

7.1 基础 generateStaticParams

typescript
// app/posts/[id]/page.tsx
interface Post {
  id: string
  title: string
  content: string
}

async function getPost(id: string): Promise<Post> {
  const res = await fetch(`https://api.example.com/posts/${id}`)
  return res.json()
}

interface PageProps {
  params: Promise<{ id: string }>
}

export default async function PostPage({ params }: PageProps) {
  const { id } = await params
  const post = await getPost(id)
  
  return (
    <article className="container mx-auto p-4 max-w-3xl">
      <h1 className="text-4xl font-bold mb-6">{post.title}</h1>
      <div className="prose max-w-none">{post.content}</div>
    </article>
  )
}

// 生成静态参数
export async function generateStaticParams() {
  const res = await fetch('https://api.example.com/posts')
  const posts: Post[] = await res.json()
  
  return posts.map(post => ({
    id: post.id
  }))
}

// 配置动态参数行为
export const dynamicParams = true // 允许动态参数 (默认)
// export const dynamicParams = false // 只允许预生成的参数

7.2 多级动态路由

typescript
// app/blog/[category]/[slug]/page.tsx
interface Post {
  category: string
  slug: string
  title: string
  content: string
}

async function getPost(category: string, slug: string): Promise<Post> {
  const res = await fetch(
    `https://api.example.com/posts/${category}/${slug}`
  )
  return res.json()
}

interface PageProps {
  params: Promise<{
    category: string
    slug: string
  }>
}

export default async function BlogPostPage({ params }: PageProps) {
  const { category, slug } = await params
  const post = await getPost(category, slug)
  
  return (
    <article className="container mx-auto p-4 max-w-3xl">
      <nav className="mb-4 text-sm text-gray-600">
        <a href="/blog">Blog</a> /
        <a href={`/blog/${category}`}>{category}</a> /
        {slug}
      </nav>
      <h1 className="text-4xl font-bold mb-6">{post.title}</h1>
      <div className="prose max-w-none">{post.content}</div>
    </article>
  )
}

export async function generateStaticParams() {
  const res = await fetch('https://api.example.com/posts')
  const posts: Post[] = await res.json()
  
  return posts.map(post => ({
    category: post.category,
    slug: post.slug
  }))
}

7.3 父子路由参数生成

typescript
// app/categories/[category]/products/[id]/page.tsx
interface PageProps {
  params: Promise<{
    category: string
    id: string
  }>
}

export default async function ProductPage({ params }: PageProps) {
  const { category, id } = await params
  
  return (
    <div>
      Product {id} in {category}
    </div>
  )
}

// 父路由生成参数
// app/categories/[category]/page.tsx
export async function generateStaticParams() {
  return [
    { category: 'electronics' },
    { category: 'clothing' },
    { category: 'books' }
  ]
}

// 子路由生成参数 (接收父路由参数)
// app/categories/[category]/products/[id]/page.tsx
export async function generateStaticParams({
  params
}: {
  params: { category: string }
}) {
  const res = await fetch(
    `https://api.example.com/categories/${params.category}/products`
  )
  const products = await res.json()
  
  return products.map((product: any) => ({
    id: product.id
  }))
}

7.4 增量静态生成

typescript
// app/posts/[id]/page.tsx
export const revalidate = 60 // 每60秒重新验证

export async function generateStaticParams() {
  // 只预生成最热门的文章
  const res = await fetch('https://api.example.com/posts/popular?limit=10')
  const posts = await res.json()
  
  return posts.map((post: any) => ({
    id: post.id
  }))
}

// 允许动态参数
export const dynamicParams = true

interface PageProps {
  params: Promise<{ id: string }>
}

export default async function PostPage({ params }: PageProps) {
  const { id } = await params
  
  // 如果 ID 不在预生成列表中,会在首次访问时生成
  const res = await fetch(`https://api.example.com/posts/${id}`)
  const post = await res.json()
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}

八、实战案例

8.1 多语言博客系统

typescript
// 文件结构:
// app/
//   [lang]/
//     blog/
//       [[...slug]]/
//         page.tsx
//     layout.tsx

// app/[lang]/layout.tsx
import { notFound } from 'next/navigation'

const languages = ['en', 'zh', 'ja', 'ko']

interface LayoutProps {
  children: React.ReactNode
  params: Promise<{ lang: string }>
}

export default async function LangLayout({ children, params }: LayoutProps) {
  const { lang } = await params
  
  if (!languages.includes(lang)) {
    notFound()
  }
  
  return (
    <html lang={lang}>
      <body>
        <nav className="bg-white border-b">
          <div className="container mx-auto px-4 py-4">
            <div className="flex items-center justify-between">
              <a href={`/${lang}`} className="text-2xl font-bold">
                Blog
              </a>
              <div className="flex gap-4">
                {languages.map(l => (
                  <a
                    key={l}
                    href={`/${l}/blog`}
                    className={`px-3 py-1 rounded ${
                      l === lang
                        ? 'bg-blue-500 text-white'
                        : 'hover:bg-gray-100'
                    }`}
                  >
                    {l.toUpperCase()}
                  </a>
                ))}
              </div>
            </div>
          </div>
        </nav>
        {children}
      </body>
    </html>
  )
}

export async function generateStaticParams() {
  return languages.map(lang => ({ lang }))
}

// app/[lang]/blog/[[...slug]]/page.tsx
import { notFound } from 'next/navigation'

interface PageProps {
  params: Promise<{
    lang: string
    slug?: string[]
  }>
}

async function getPost(lang: string, slug: string[]) {
  const path = slug.join('/')
  const res = await fetch(
    `https://api.example.com/${lang}/posts/${path}`
  )
  
  if (!res.ok) return null
  
  return res.json()
}

async function getPosts(lang: string) {
  const res = await fetch(`https://api.example.com/${lang}/posts`)
  return res.json()
}

export default async function BlogPage({ params }: PageProps) {
  const { lang, slug } = await params
  
  if (!slug || slug.length === 0) {
    // 博客列表
    const posts = await getPosts(lang)
    
    return (
      <div className="container mx-auto p-4">
        <h1 className="text-4xl font-bold mb-6">
          {lang === 'en' && 'Blog Posts'}
          {lang === 'zh' && '博客文章'}
          {lang === 'ja' && 'ブログ記事'}
          {lang === 'ko' && '블로그 게시물'}
        </h1>
        <div className="grid md:grid-cols-3 gap-6">
          {posts.map((post: any) => (
            <a
              key={post.slug}
              href={`/${lang}/blog/${post.slug}`}
              className="border rounded-lg p-4 hover:shadow-lg transition"
            >
              <h2 className="text-xl font-semibold mb-2">{post.title}</h2>
              <p className="text-gray-600">{post.excerpt}</p>
            </a>
          ))}
        </div>
      </div>
    )
  }
  
  // 单篇文章
  const post = await getPost(lang, slug)
  
  if (!post) {
    notFound()
  }
  
  return (
    <article className="container mx-auto p-4 max-w-3xl">
      <h1 className="text-4xl font-bold mb-6">{post.title}</h1>
      <div className="prose max-w-none">{post.content}</div>
    </article>
  )
}

export async function generateStaticParams() {
  const languages = ['en', 'zh', 'ja', 'ko']
  const params = []
  
  for (const lang of languages) {
    const res = await fetch(`https://api.example.com/${lang}/posts`)
    const posts = await res.json()
    
    // 博客列表页
    params.push({ lang, slug: undefined })
    
    // 各个文章页
    for (const post of posts) {
      params.push({
        lang,
        slug: post.slug.split('/')
      })
    }
  }
  
  return params
}

8.2 电商产品路由

typescript
// 文件结构:
// app/
//   shop/
//     [[...path]]/
//       page.tsx

// app/shop/[[...path]]/page.tsx
import { notFound } from 'next/navigation'

interface PageProps {
  params: Promise<{
    path?: string[]
  }>
  searchParams: Promise<{
    sort?: string
    minPrice?: string
    maxPrice?: string
    page?: string
  }>
}

async function getCategories() {
  const res = await fetch('https://api.example.com/categories')
  return res.json()
}

async function getProducts(filters: any) {
  const params = new URLSearchParams(filters)
  const res = await fetch(`https://api.example.com/products?${params}`)
  return res.json()
}

async function getProduct(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`)
  if (!res.ok) return null
  return res.json()
}

export default async function ShopPage({ params, searchParams }: PageProps) {
  const { path = [] } = await params
  const filters = await searchParams
  
  // 路由: /shop
  if (path.length === 0) {
    const categories = await getCategories()
    
    return (
      <div className="container mx-auto p-4">
        <h1 className="text-4xl font-bold mb-6">Shop</h1>
        <div className="grid md:grid-cols-4 gap-6">
          {categories.map((category: any) => (
            <a
              key={category.slug}
              href={`/shop/${category.slug}`}
              className="border rounded-lg p-6 hover:shadow-lg transition"
            >
              <img
                src={category.image}
                alt={category.name}
                className="w-full h-48 object-cover rounded mb-4"
              />
              <h2 className="text-xl font-semibold">{category.name}</h2>
              <p className="text-gray-600">{category.count} products</p>
            </a>
          ))}
        </div>
      </div>
    )
  }
  
  // 路由: /shop/[category]
  if (path.length === 1) {
    const [category] = path
    const products = await getProducts({ category, ...filters })
    
    return (
      <div className="container mx-auto p-4">
        <nav className="mb-4 text-sm text-gray-600">
          <a href="/shop">Shop</a> / {category}
        </nav>
        
        <h1 className="text-4xl font-bold mb-6 capitalize">{category}</h1>
        
        <div className="flex gap-8">
          <aside className="w-64">
            <div className="mb-6">
              <h3 className="font-semibold mb-2">Sort By</h3>
              <select className="w-full p-2 border rounded">
                <option value="relevance">Relevance</option>
                <option value="price-asc">Price: Low to High</option>
                <option value="price-desc">Price: High to Low</option>
              </select>
            </div>
          </aside>
          
          <main className="flex-1">
            <div className="grid md:grid-cols-3 gap-6">
              {products.items.map((product: any) => (
                <a
                  key={product.id}
                  href={`/shop/${category}/${product.slug}`}
                  className="border rounded-lg overflow-hidden hover:shadow-lg transition"
                >
                  <img
                    src={product.image}
                    alt={product.name}
                    className="w-full h-48 object-cover"
                  />
                  <div className="p-4">
                    <h3 className="font-semibold mb-2">{product.name}</h3>
                    <p className="text-blue-600 font-bold">${product.price}</p>
                  </div>
                </a>
              ))}
            </div>
          </main>
        </div>
      </div>
    )
  }
  
  // 路由: /shop/[category]/[slug]
  if (path.length === 2) {
    const [category, slug] = path
    const product = await getProduct(slug)
    
    if (!product) {
      notFound()
    }
    
    return (
      <div className="container mx-auto p-4">
        <nav className="mb-4 text-sm text-gray-600">
          <a href="/shop">Shop</a> /
          <a href={`/shop/${category}`}>{category}</a> /
          {product.name}
        </nav>
        
        <div className="grid md:grid-cols-2 gap-8">
          <div>
            <img
              src={product.image}
              alt={product.name}
              className="w-full rounded-lg"
            />
          </div>
          <div>
            <h1 className="text-4xl font-bold mb-4">{product.name}</h1>
            <p className="text-3xl text-blue-600 mb-6">${product.price}</p>
            <p className="text-gray-600 mb-6">{product.description}</p>
            <button className="w-full px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
              Add to Cart
            </button>
          </div>
        </div>
      </div>
    )
  }
  
  notFound()
}

export async function generateStaticParams() {
  const categories = await getCategories()
  const params = []
  
  // 生成分类页
  for (const category of categories) {
    params.push({ path: [category.slug] })
    
    // 生成产品页
    const products = await getProducts({ category: category.slug })
    for (const product of products.items) {
      params.push({ path: [category.slug, product.slug] })
    }
  }
  
  return params
}

九、总结与最佳实践

9.1 路由设计原则

typescript
// 1. 保持 URL 结构清晰
// 好: /blog/2024/nextjs-15
// 坏: /b/24/n15

// 2. 使用语义化路径
// 好: /products/electronics/phones
// 坏: /p/1/2

// 3. 避免过深的嵌套
// 好: /docs/api/components
// 坏: /docs/v1/api/reference/components/ui/buttons

// 4. 使用一致的命名
// 好: /products/[id] 和 /users/[id]
// 坏: /products/[productId] 和 /users/[userId]

// 5. 合理使用捕获所有段
// 好: /docs/[[...slug]] (灵活的文档系统)
// 坏: /[[...path]] (过于宽泛)

9.2 性能优化

typescript
// 1. 使用 generateStaticParams 预生成热门页面
export async function generateStaticParams() {
  const popular = await getPopularPosts()
  return popular.map(post => ({ id: post.id }))
}

// 2. 启用增量静态生成
export const revalidate = 3600 // 1小时

// 3. 合理使用 dynamicParams
export const dynamicParams = true // 允许动态生成

// 4. 并行数据获取
const [post, comments, related] = await Promise.all([
  getPost(id),
  getComments(id),
  getRelatedPosts(id)
])

// 5. 使用 Suspense 流式渲染
<Suspense fallback={<Loading />}>
  <SlowComponent />
</Suspense>

9.3 常见问题

问题原因解决方案
404 错误路由不匹配检查文件结构和参数名称
参数为 undefined未正确 await params使用 const { id } = await params
路由冲突多个动态段冲突使用路由组或调整结构
静态生成失败generateStaticParams 错误检查返回格式和数据
性能问题过度动态渲染使用静态生成和缓存

9.4 学习资源

  1. 官方文档

  2. 实践项目

    • 多语言博客
    • 电商平台
    • 文档系统
    • 文件浏览器

课后练习

  1. 创建一个多级分类的电商路由系统
  2. 实现一个带模态框的图片画廊
  3. 构建一个多语言文档系统
  4. 开发一个文件浏览器应用
  5. 实现一个带筛选的产品搜索页面

通过本课程的学习,你应该能够熟练掌握 Next.js 中的动态路由和参数处理,并能根据实际需求设计合理的路由结构。记住:良好的路由设计是用户体验的基础!