在现代 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 目录不算作路由段。
三、典型使用场景
- 图片/视频预览 Modal(如 Instagram)
- 登录/注册弹窗(同时有独立
/login页面) - 购物车侧边栏(可分享
/cart链接) - 文章详情内嵌预览
四、实战示例:实现一个可分享的图片 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 不支持。