从0死磕全栈之Next.js 拦截路由(Intercepting Routes)详解:实现模态框与上下文保持的利器

133 阅读4分钟

在现代 Web 应用中,我们经常需要在不跳转页面的情况下展示某些内容,比如点击图片弹出一个模态框(Modal),但又希望这个模态框的内容可以通过 URL 分享、刷新后依然存在,甚至支持浏览器前进/后退操作。Next.js 13+ 的 App Router 提供了一个强大的功能 —— 拦截路由(Intercepting Routes),完美解决这类需求。

本文将深入浅出地介绍拦截路由的概念、使用场景,并提供可直接运行的代码示例。


一、什么是拦截路由(Intercepting Routes)?

拦截路由允许你在当前布局中加载另一个路由的内容,而不真正跳转到那个路由。URL 会被“伪装”成目标路径,但页面结构仍保留在当前上下文中。

核心特点:

  • 软导航(Soft Navigation):点击链接时,内容以内联方式(如 Modal)展示,URL 变化但页面不刷新。
  • 硬导航(Hard Navigation):直接访问 URL 或刷新页面时,渲染完整的目标页面。
  • URL 可分享:Modal 中的内容拥有独立 URL,用户可直接分享。
  • 浏览器历史兼容:支持前进/后退操作,Modal 会自动打开或关闭。

✅ 举个例子:
/feed 页面点击一张图片,URL 变为 /photo/123,但页面仍显示在 /feed 的上下文中(比如以 Modal 形式展示照片)。
而如果你直接在浏览器输入 /photo/123 并回车,则会渲染完整的照片页面。


二、拦截路由的命名约定

Next.js 使用特殊的目录命名规则来定义拦截路由:

语法含义
(.)拦截同级路由
(..)拦截上一级路由
(..)(..)拦截上两级路由
(...)拦截根目录下的路由

⚠️ 注意:这里的“级”指的是 路由段(route segments),不是文件系统层级。例如 @modal 这样的 Parallel Routes 目录不算作路由段。


三、典型使用场景

  1. 图片/视频预览 Modal(如 Instagram)
  2. 登录/注册弹窗(同时有独立 /login 页面)
  3. 购物车侧边栏(可分享 /cart 链接)
  4. 文章详情内嵌预览

四、实战示例:实现一个可分享的图片 Modal

假设我们有以下页面结构:

app/
├── feed/
│   └── page.tsx          # 图片流页面
├── photo/
│   └── [id]/
│       └── page.tsx      # 独立图片详情页
└── (..)photo/            # ← 拦截路由!
    └── [id]/
        └── page.tsx      # 在 feed 中以 Modal 展示

1. 创建主页面:app/feed/page.tsx

// app/feed/page.tsx
export default function Feed() {
  return (
    <div>
      <h1>图片流</h1>
      <ul>
        {[1, 2, 3].map((id) => (
          <li key={id}>
            {/* 使用 next/link 软导航到 /photo/1 */}
            <a href={`/photo/${id}`}>查看图片 {id}</a>
          </li>
        ))}
      </ul>
    </div>
  );
}

2. 创建独立图片页:app/photo/[id]/page.tsx

// app/photo/[id]/page.tsx
export default function PhotoPage({ params }: { params: { id: string } }) {
  return (
    <div style={{ padding: '20px', border: '1px solid #ccc' }}>
      <h2>独立图片页面 - ID: {params.id}</h2>
      <p>这是完整的图片详情页,可直接访问或刷新。</p>
    </div>
  );
}

3. 创建拦截路由:app/(..)photo/[id]/page.tsx

注意目录名是 (..)photo,表示“拦截上一级的 photo 路由”

// app/(..)photo/[id]/page.tsx
'use client';

import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export default function InterceptedPhoto({ params }: { params: { id: string } }) {
  const router = useRouter();

  // 可选:监听 ESC 关闭 Modal
  useEffect(() => {
    const handleEsc = (e: KeyboardEvent) => {
      if (e.key === 'Escape') router.back();
    };
    window.addEventListener('keydown', handleEsc);
    return () => window.removeEventListener('keydown', handleEsc);
  }, [router]);

  return (
    <div
      style={{
        position: 'fixed',
        top: 0,
        left: 0,
        width: '100vw',
        height: '100vh',
        background: 'rgba(0,0,0,0.8)',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        zIndex: 1000,
      }}
      onClick={() => router.back()} // 点击背景关闭
    >
      <div
        style={{
          background: 'white',
          padding: '20px',
          borderRadius: '8px',
          maxWidth: '500px',
          width: '90%',
        }}
        onClick={(e) => e.stopPropagation()} // 阻止冒泡
      >
        <h2>Modal 中的图片 - ID: {params.id}</h2>
        <p>这是在 /feed 页面中拦截展示的 Modal 内容。</p>
        <button onClick={() => router.back()}>关闭</button>
      </div>
    </div>
  );
}

4. 效果说明

  • 当你在 /feed 页面点击“查看图片 1”,URL 变为 /photo/1,但页面显示的是 Modal。
  • 如果你直接访问 /photo/1,则渲染的是完整的 app/photo/[id]/page.tsx
  • 刷新页面时,Modal 自动消失,显示完整页面(符合预期)。
  • 浏览器后退按钮会关闭 Modal,而不是跳转到上一个页面。

五、高级技巧:与 Parallel Routes 结合

你还可以将拦截路由与 Parallel Routes 结合,实现更复杂的布局。例如:

app/
├── @modal/
│   └── default.tsx       # 默认空 slot
├── feed/
│   └── page.tsx
├── (..)photo/
│   └── [id]/
│       └── page.tsx      # 渲染到 @modal slot

这样,Modal 内容可以被精准插入到指定的 UI 区域,而无需手动管理状态。具体内容将会在下一届具体讲解。


六、总结

拦截路由是 Next.js App Router 中一个优雅且强大的功能,它解决了传统 Modal 组件无法分享 URL、刷新丢失状态、历史记录混乱等问题。

关键优势:

  • ✅ URL 可分享
  • ✅ 刷新保持上下文(自动降级为完整页面)
  • ✅ 原生支持浏览器前进/后退
  • ✅ 无需额外状态管理

如果你正在构建一个需要“内联展示但可独立访问”的功能,强烈推荐使用拦截路由!


📌 提示:拦截路由仅在 App Router(app/ 目录)中可用,Pages Router 不支持。