Next.js 面试题详细答案 - Q14
Q14: 什么是并行路由(Parallel Routes)和拦截路由(Intercepted Routes)?请描述一个使用场景(如:模态框中的动态内容)。
并行路由(Parallel Routes)
并行路由允许在同一个布局中同时渲染多个页面,每个页面都有自己独立的加载状态和错误处理。
基本概念
// app/layout.js - 根布局
export default function RootLayout({ children, modal, sidebar }) {
return (
<html>
<body>
<div className="app">
<aside>{sidebar}</aside>
<main>{children}</main>
{modal}
</div>
</body>
</html>
)
}
// 文件结构
app/
├── layout.js
├── page.js
├── @modal/
│ ├── default.js
│ └── (..)photo/
│ └── [id]/
│ └── page.js
├── @sidebar/
│ ├── default.js
│ └── page.js
└── photo/
└── [id]/
└── page.js
实际应用示例
// app/layout.js - 主布局
export default function Layout({ children, modal, sidebar }) {
return (
<div className="app-layout">
<header>
<h1>我的应用</h1>
</header>
<div className="content">
<aside className="sidebar">
{sidebar}
</aside>
<main className="main-content">
{children}
</main>
</div>
{modal}
</div>
)
}
// app/@sidebar/default.js - 侧边栏默认内容
export default function SidebarDefault() {
return (
<div className="sidebar-default">
<h3>导航菜单</h3>
<ul>
<li><a href="/">首页</a></li>
<li><a href="/photos">照片</a></li>
<li><a href="/about">关于</a></li>
</ul>
</div>
)
}
// app/@sidebar/page.js - 侧边栏页面内容
export default function SidebarPage() {
return (
<div className="sidebar-page">
<h3>侧边栏内容</h3>
<p>这是侧边栏的页面内容</p>
</div>
)
}
// app/@modal/default.js - 模态框默认内容
export default function ModalDefault() {
return null // 默认不显示模态框
}
// app/@modal/(..)photo/[id]/page.js - 拦截路由
export default function PhotoModal({ params }) {
return (
<div className="modal-overlay">
<div className="modal-content">
<h2>照片详情</h2>
<p>照片 ID: {params.id}</p>
<button>关闭</button>
</div>
</div>
)
}
拦截路由(Intercepted Routes)
拦截路由允许在用户导航到某个页面时,在当前位置显示该页面的内容,而不是跳转到新页面。
基本概念
// 拦截路由语法
@modal/
├── (..)photo/ // 拦截同一级别的 photo 路由
├── (.)photo/ // 拦截当前级别的 photo 路由
├── (..)photo/ // 拦截上一级别的 photo 路由
└── (...photo)/ // 拦截根级别的 photo 路由
实际应用示例
// app/photo/[id]/page.js - 照片详情页
export default function PhotoPage({ params }) {
return (
<div className="photo-page">
<h1>照片详情</h1>
<img src={`/photos/${params.id}.jpg`} alt="照片" />
<p>照片 ID: {params.id}</p>
<p>这是照片的详细信息页面</p>
</div>
)
}
// app/@modal/(..)photo/[id]/page.js - 拦截路由
export default function PhotoModal({ params }) {
return (
<div className="modal-overlay">
<div className="modal-content">
<h2>照片预览</h2>
<img src={`/photos/${params.id}.jpg`} alt="照片" />
<p>照片 ID: {params.id}</p>
<div className="modal-actions">
<button>查看详情</button>
<button>关闭</button>
</div>
</div>
</div>
)
}
完整应用示例:照片画廊
1. 文件结构
app/
├── layout.js
├── page.js
├── @modal/
│ ├── default.js
│ └── (..)photo/
│ └── [id]/
│ ├── page.js
│ ├── loading.js
│ └── error.js
├── @sidebar/
│ ├── default.js
│ └── page.js
└── photo/
└── [id]/
├── page.js
├── loading.js
└── error.js
2. 主布局
// app/layout.js
export default function Layout({ children, modal, sidebar }) {
return (
<html>
<body>
<div className="app">
<header>
<h1>照片画廊</h1>
</header>
<div className="content">
<aside className="sidebar">
{sidebar}
</aside>
<main className="main-content">
{children}
</main>
</div>
{modal}
</div>
</body>
</html>
)
}
3. 侧边栏组件
// app/@sidebar/default.js
export default function SidebarDefault() {
return (
<div className="sidebar">
<h3>照片分类</h3>
<ul>
<li><a href="/">全部照片</a></li>
<li><a href="/?category=nature">自然风光</a></li>
<li><a href="/?category=urban">城市建筑</a></li>
<li><a href="/?category=portrait">人像摄影</a></li>
</ul>
</div>
)
}
// app/@sidebar/page.js
export default function SidebarPage() {
return (
<div className="sidebar">
<h3>照片分类</h3>
<ul>
<li><a href="/">全部照片</a></li>
<li><a href="/?category=nature">自然风光</a></li>
<li><a href="/?category=urban">城市建筑</a></li>
<li><a href="/?category=portrait">人像摄影</a></li>
</ul>
<div className="sidebar-content">
<h4>侧边栏页面内容</h4>
<p>这里可以显示额外的信息或功能</p>
</div>
</div>
)
}
4. 模态框组件
// app/@modal/default.js
export default function ModalDefault() {
return null // 默认不显示模态框
}
// app/@modal/(..)photo/[id]/page.js
'use client'
import { useRouter } from 'next/navigation'
export default function PhotoModal({ params }) {
const router = useRouter()
const handleClose = () => {
router.back()
}
const handleViewDetails = () => {
router.push(`/photo/${params.id}`)
}
return (
<div className="modal-overlay" onClick={handleClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>照片预览</h2>
<button onClick={handleClose} className="close-btn">
×
</button>
</div>
<div className="modal-body">
<img
src={`/photos/${params.id}.jpg`}
alt={`照片 ${params.id}`}
className="modal-image"
/>
<p>照片 ID: {params.id}</p>
</div>
<div className="modal-footer">
<button onClick={handleViewDetails}>
查看详情
</button>
<button onClick={handleClose}>
关闭
</button>
</div>
</div>
</div>
)
}
// app/@modal/(..)photo/[id]/loading.js
export default function ModalLoading() {
return (
<div className="modal-overlay">
<div className="modal-content">
<div className="loading">
<div className="spinner"></div>
<p>加载中...</p>
</div>
</div>
</div>
)
}
// app/@modal/(..)photo/[id]/error.js
'use client'
import { useRouter } from 'next/navigation'
export default function ModalError({ error, reset }) {
const router = useRouter()
const handleClose = () => {
router.back()
}
return (
<div className="modal-overlay">
<div className="modal-content">
<div className="error">
<h2>加载失败</h2>
<p>{error.message}</p>
<div className="error-actions">
<button onClick={reset}>重试</button>
<button onClick={handleClose}>关闭</button>
</div>
</div>
</div>
</div>
)
}
5. 照片详情页
// app/photo/[id]/page.js
export default function PhotoPage({ params }) {
return (
<div className="photo-page">
<h1>照片详情</h1>
<img
src={`/photos/${params.id}.jpg`}
alt={`照片 ${params.id}`}
className="photo-image"
/>
<div className="photo-info">
<h2>照片 {params.id}</h2>
<p>这是照片的详细信息页面</p>
<p>拍摄时间: 2024年1月1日</p>
<p>拍摄地点: 某个美丽的地方</p>
</div>
</div>
)
}
// app/photo/[id]/loading.js
export default function PhotoLoading() {
return (
<div className="photo-page">
<div className="loading">
<div className="spinner"></div>
<p>加载照片中...</p>
</div>
</div>
)
}
// app/photo/[id]/error.js
'use client'
export default function PhotoError({ error, reset }) {
return (
<div className="photo-page">
<div className="error">
<h2>照片加载失败</h2>
<p>{error.message}</p>
<button onClick={reset}>重试</button>
</div>
</div>
)
}
6. 首页组件
// app/page.js
import Link from 'next/link'
export default function HomePage() {
const photos = [
{ id: 1, title: '照片 1' },
{ id: 2, title: '照片 2' },
{ id: 3, title: '照片 3' },
{ id: 4, title: '照片 4' },
]
return (
<div className="home-page">
<h1>照片画廊</h1>
<div className="photo-grid">
{photos.map((photo) => (
<div key={photo.id} className="photo-item">
<Link href={`/photo/${photo.id}`}>
<img
src={`/photos/${photo.id}.jpg`}
alt={photo.title}
className="photo-thumbnail"
/>
<h3>{photo.title}</h3>
</Link>
</div>
))}
</div>
</div>
)
}
使用场景总结
1. 并行路由适用场景
- 仪表盘应用:侧边栏 + 主内容 + 通知面板
- 电商网站:产品列表 + 筛选面板 + 购物车
- 博客系统:文章列表 + 分类导航 + 相关文章
2. 拦截路由适用场景
- 模态框:照片预览、用户详情、设置面板
- 侧边栏:用户资料、购物车、通知列表
- 覆盖层:登录表单、确认对话框、快速编辑
3. 优势
- 更好的用户体验:无需页面跳转即可查看内容
- 保持上下文:用户不会丢失当前位置
- 性能优化:只加载必要的组件
- 灵活布局:可以同时显示多个内容区域
总结
并行路由和拦截路由是 Next.js 13+ 的强大功能:
- 并行路由:允许在同一个布局中同时渲染多个页面
- 拦截路由:允许在当前位置显示其他页面的内容
- 组合使用:可以创建复杂的用户界面,如模态框、侧边栏等
- 用户体验:提供更流畅的导航和交互体验
这些功能让开发者能够创建更现代、更用户友好的 Web 应用。