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>
)
}
总结
动态路由的工作原理:
- 文件系统路由:使用方括号
[]创建动态路径段 - 参数传递:通过
params对象传递动态参数 - 静态生成:使用
generateStaticParams预生成静态页面 - 错误处理:使用
notFound()和错误边界处理异常情况 - 性能优化:结合 ISR 和缓存策略提高性能
动态路由是 Next.js 中创建灵活、可扩展应用的重要工具,特别适合内容管理系统、电商网站和文档系统等应用场景。