Q16: 如何设置动态路由?app/blog/[slug]/page.js 这样的结构是如何工作的?

29 阅读2分钟

Next.js 面试题详细答案 - Q16

Q16: 如何设置动态路由?app/blog/[slug]/page.js 这样的结构是如何工作的?

动态路由基本概念

动态路由使用方括号 [] 语法来创建动态路径段,允许在运行时根据参数生成不同的页面。

基本语法

1. 单个动态参数
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
  return (
    <div>
      <h1>博客文章</h1>
      <p>文章 slug: {params.slug}</p>
    </div>
  )
}

// URL 示例
// /blog/hello-world -> params.slug = "hello-world"
// /blog/my-first-post -> params.slug = "my-first-post"
2. 多个动态参数
// app/blog/[category]/[slug]/page.js
export default function BlogPost({ params }) {
  return (
    <div>
      <h1>博客文章</h1>
      <p>分类: {params.category}</p>
      <p>文章 slug: {params.slug}</p>
    </div>
  )
}

// URL 示例
// /blog/tech/nextjs-tutorial -> params.category = "tech", params.slug = "nextjs-tutorial"
// /blog/life/my-journey -> params.category = "life", params.slug = "my-journey"
3. 捕获所有路由
// app/docs/[...slug]/page.js
export default function DocPage({ params }) {
  return (
    <div>
      <h1>文档页面</h1>
      <p>路径: {params.slug.join('/')}</p>
    </div>
  )
}

// URL 示例
// /docs/getting-started -> params.slug = ["getting-started"]
// /docs/api/authentication -> params.slug = ["api", "authentication"]
// /docs/guides/react/hooks -> params.slug = ["guides", "react", "hooks"]
4. 可选捕获路由
// app/shop/[[...slug]]/page.js
export default function ShopPage({ params }) {
  const slug = params.slug || []

  return (
    <div>
      <h1>商店页面</h1>
      <p>路径: {slug.join('/') || '首页'}</p>
    </div>
  )
}

// URL 示例
// /shop -> params.slug = undefined
// /shop/electronics -> params.slug = ["electronics"]
// /shop/electronics/phones -> params.slug = ["electronics", "phones"]

实际应用示例

1. 博客系统
// app/blog/[slug]/page.js
export async function generateStaticParams() {
  const posts = await fetch('https://jsonplaceholder.typicode.com/posts')
  const data = await posts.json()

  return data.map((post) => ({
    slug: post.id.toString(),
  }))
}

async function BlogPost({ params }) {
  const post = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.slug}`
  )
  const data = await post.json()

  if (!data) {
    notFound()
  }

  return (
    <article>
      <h1>{data.title}</h1>
      <p>文章 ID: {params.slug}</p>
      <div>{data.body}</div>
    </article>
  )
}

export default BlogPost
2. 电商产品页面
// app/products/[category]/[id]/page.js
export async function generateStaticParams() {
  const categories = await fetch('https://api.example.com/categories')
  const categoriesData = await categories.json()

  const params = []

  for (const category of categoriesData) {
    const products = await fetch(
      `https://api.example.com/categories/${category.id}/products`
    )
    const productsData = await products.json()

    for (const product of productsData) {
      params.push({
        category: category.slug,
        id: product.id.toString(),
      })
    }
  }

  return params
}

async function ProductPage({ params }) {
  const product = await fetch(
    `https://api.example.com/products/${params.category}/${params.id}`
  )
  const data = await product.json()

  if (!data) {
    notFound()
  }

  return (
    <div>
      <h1>{data.name}</h1>
      <p>分类: {params.category}</p>
      <p>产品 ID: {params.id}</p>
      <p>价格: ${data.price}</p>
      <p>描述: {data.description}</p>
    </div>
  )
}

export default ProductPage
3. 用户资料页面
// app/users/[id]/page.js
export async function generateStaticParams() {
  const users = await fetch('https://jsonplaceholder.typicode.com/users')
  const data = await users.json()

  return data.map((user) => ({
    id: user.id.toString(),
  }))
}

async function UserProfile({ params }) {
  const user = await fetch(
    `https://jsonplaceholder.typicode.com/users/${params.id}`
  )
  const data = await user.json()

  if (!data) {
    notFound()
  }

  return (
    <div>
      <h1>{data.name}</h1>
      <p>用户 ID: {params.id}</p>
      <p>邮箱: {data.email}</p>
      <p>电话: {data.phone}</p>
      <p>网站: {data.website}</p>
    </div>
  )
}

export default UserProfile
4. 文档系统
// app/docs/[...slug]/page.js
export async function generateStaticParams() {
  const docs = await fetch('https://api.example.com/docs')
  const data = await docs.json()

  return data.map((doc) => ({
    slug: doc.path.split('/'),
  }))
}

async function DocPage({ params }) {
  const path = params.slug.join('/')
  const doc = await fetch(`https://api.example.com/docs/${path}`)
  const data = await doc.json()

  if (!data) {
    notFound()
  }

  return (
    <div>
      <h1>{data.title}</h1>
      <div className="breadcrumb">
        {params.slug.map((segment, index) => (
          <span key={index}>
            {segment}
            {index < params.slug.length - 1 && ' / '}
          </span>
        ))}
      </div>
      <div className="content">{data.content}</div>
    </div>
  )
}

export default DocPage

高级用法

1. 动态路由与搜索参数
// app/blog/[slug]/page.js
'use client'
import { useSearchParams } from 'next/navigation'

export default function BlogPost({ params }) {
  const searchParams = useSearchParams()
  const edit = searchParams.get('edit')
  const version = searchParams.get('version')

  return (
    <div>
      <h1>博客文章: {params.slug}</h1>
      {edit && <p>编辑模式</p>}
      {version && <p>版本: {version}</p>}
    </div>
  )
}

// URL 示例
// /blog/my-post?edit=true&version=2
// params.slug = "my-post"
// searchParams.get('edit') = "true"
// searchParams.get('version') = "2"
2. 动态路由与布局
// app/blog/[slug]/layout.js
export default function BlogLayout({ children, params }) {
  return (
    <div className="blog-layout">
      <header>
        <h1>博客</h1>
        <p>当前文章: {params.slug}</p>
      </header>
      <main>{children}</main>
    </div>
  )
}

// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
  return (
    <article>
      <h1>文章内容</h1>
      <p>文章 slug: {params.slug}</p>
    </article>
  )
}
3. 动态路由与中间件
// middleware.js
import { NextResponse } from 'next/server'

export function middleware(request) {
  const { pathname } = request.nextUrl

  // 检查动态路由
  if (pathname.startsWith('/blog/')) {
    const slug = pathname.split('/')[2]

    // 验证 slug 格式
    if (!/^[a-z0-9-]+$/.test(slug)) {
      return NextResponse.redirect(new URL('/blog', request.url))
    }
  }

  return NextResponse.next()
}

export const config = {
  matcher: '/blog/:path*',
}
4. 动态路由与 API 路由
// app/api/blog/[slug]/route.js
export async function GET(request, { params }) {
  const { slug } = params

  try {
    const post = await fetch(`https://api.example.com/posts/${slug}`)
    const data = await post.json()

    return Response.json(data)
  } catch (error) {
    return Response.json({ error: 'Post not found' }, { status: 404 })
  }
}

export async function PUT(request, { params }) {
  const { slug } = params
  const body = await request.json()

  try {
    const updatedPost = await updatePost(slug, body)
    return Response.json(updatedPost)
  } catch (error) {
    return Response.json({ error: 'Failed to update post' }, { status: 500 })
  }
}

错误处理

1. 404 处理
// app/blog/[slug]/page.js
import { notFound } from 'next/navigation'

export default async function BlogPost({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`)

  if (!post.ok) {
    notFound()
  }

  const data = await post.json()

  return (
    <article>
      <h1>{data.title}</h1>
      <div>{data.content}</div>
    </article>
  )
}

// app/blog/[slug]/not-found.js
export default function NotFound() {
  return (
    <div>
      <h1>文章未找到</h1>
      <p>抱歉,您要查找的文章不存在。</p>
      <a href="/blog">返回博客列表</a>
    </div>
  )
}
2. 错误边界
// app/blog/[slug]/error.js
'use client'
export default function Error({ error, reset }) {
  return (
    <div>
      <h2>加载文章时出错</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>重试</button>
    </div>
  )
}

性能优化

1. 静态生成
// app/blog/[slug]/page.js
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts')
  const data = await posts.json()

  return data.map((post) => ({
    slug: post.slug,
  }))
}

export default async function BlogPost({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
    next: { revalidate: 3600 }, // 1小时重新验证
  })
  const data = await post.json()

  return (
    <article>
      <h1>{data.title}</h1>
      <div>{data.content}</div>
    </article>
  )
}
2. 动态导入
// app/blog/[slug]/page.js
import dynamic from 'next/dynamic'

const DynamicComponent = dynamic(() => import('./DynamicComponent'), {
  loading: () => <p>加载中...</p>,
})

export default function BlogPost({ params }) {
  return (
    <div>
      <h1>文章: {params.slug}</h1>
      <DynamicComponent />
    </div>
  )
}

最佳实践

1. 参数验证
// app/blog/[slug]/page.js
export default async function BlogPost({ params }) {
  const { slug } = params

  // 验证 slug 格式
  if (!/^[a-z0-9-]+$/.test(slug)) {
    notFound()
  }

  const post = await fetch(`https://api.example.com/posts/${slug}`)
  const data = await post.json()

  return (
    <article>
      <h1>{data.title}</h1>
      <div>{data.content}</div>
    </article>
  )
}
2. 类型安全
// types/blog.ts
export interface BlogPostParams {
  slug: string;
}

export interface BlogPostProps {
  params: BlogPostParams;
}

// app/blog/[slug]/page.js
import { BlogPostProps } from '@/types/blog'

export default async function BlogPost({ params }: BlogPostProps) {
  const { slug } = params

  const post = await fetch(`https://api.example.com/posts/${slug}`)
  const data = await post.json()

  return (
    <article>
      <h1>{data.title}</h1>
      <div>{data.content}</div>
    </article>
  )
}

总结

动态路由的工作原理:

  1. 文件系统路由:使用方括号 [] 创建动态路径段
  2. 参数传递:通过 params 对象传递动态参数
  3. 静态生成:使用 generateStaticParams 预生成静态页面
  4. 错误处理:使用 notFound() 和错误边界处理异常情况
  5. 性能优化:结合 ISR 和缓存策略提高性能

动态路由是 Next.js 中创建灵活、可扩展应用的重要工具,特别适合内容管理系统、电商网站和文档系统等应用场景。