Q14: 什么是并行路由(Parallel Routes)和拦截路由(Intercepted Routes)?请描述一个使用场景(如:模态框中的动态内容)。

52 阅读3分钟

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+ 的强大功能:

  1. 并行路由:允许在同一个布局中同时渲染多个页面
  2. 拦截路由:允许在当前位置显示其他页面的内容
  3. 组合使用:可以创建复杂的用户界面,如模态框、侧边栏等
  4. 用户体验:提供更流畅的导航和交互体验

这些功能让开发者能够创建更现代、更用户友好的 Web 应用。