Next.js的Intercepting Routes(拦截路由)
Next.js 的 Intercepting Routes(拦截路由) 是 App Router 中一个非常强大且独特的功能。
简单来说,它允许你在当前布局(Layout)中加载另一个路由,就像是把那个页面“拦截”并嵌入到了当前页面中,而不是完全跳转过去。
核心概念:软导航 vs 硬导航
拦截路由最典型的行为差异在于用户是如何访问该 URL 的:
- 软导航(点击 Link 跳转): 当你通过
<Link />点击进入该路由时,Next.js 会拦截这个请求,并在当前页面保留上下文的同时,展示目标页面的内容(通常以模态框 Modal 的形式)。 - 硬导航(刷新或直接访问 URL): 当你刷新页面、或者直接把 URL 发给朋友打开时,拦截不会发生。Next.js 会渲染该路由原本的独立页面。
经典使用场景
- Instagram/Pinterest 图片流:
- 你在浏览图片列表 (
/feed)。 - 点击一张图片,URL 变为
/photo/123,但背景依然是列表,图片以弹窗形式浮在上面。 - 如果你刷新这个
/photo/123页面,或者把链接发给别人,打开后看到的是一张完整的独立图片详情页,没有背景列表。
- 你在浏览图片列表 (
- 登录弹窗:
- 在任何页面点击“登录”,URL 变为
/login,弹出登录框,背景依然是刚才浏览的页面。 - 直接访问
/login则显示全屏登录页。
- 在任何页面点击“登录”,URL 变为
路由匹配约定
拦截路由使用特殊的文件夹命名语法,类似于文件系统的相对路径:
(.):匹配同一层级的路由。(..):匹配上一层级的路由。(..)(..):匹配上两层级的路由。(...):匹配根目录(app目录)下的路由。
实战示例:创建一个“图片详情弹窗”
为了实现“点击图片出弹窗,刷新变独立页面”的效果,我们需要结合 Parallel Routes(并行路由) 和 Intercepting Routes(拦截路由)。
假设我们要实现:
- 主页
/展示图片列表。 - 点击图片跳转
/photo/[id]。 - 在主页点击时,
/photo/[id]以 Modal 显示;直接访问时显示独立页面。
1. 目录结构
plaintext
app/
├── layout.tsx # 根布局
├── page.tsx # 主页 (显示图片列表)
├── @modal/ # 并行路由 Slot (用于放模态框)
│ ├── default.tsx # 默认处理 (返回 null)
│ └── (.)photo/[id]/ # <--- 拦截路由!拦截同一层级的 photo/[id]
│ └── page.tsx # 拦截后显示的 UI (Modal 组件)
└── photo/[id]/ # 常规路由
└── page.tsx # 独立访问时显示的 UI (全屏详情页)
2. 代码实现
A. 根布局 (app/layout.tsx)
我们需要定义 @modal 插槽来容纳拦截后的内容。
tsx
export default function Layout({
children,
modal
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<html>
<body>
{children}
{modal}
</body>
</html>
);
}
B. 主页 (app/page.tsx)
包含指向 /photo/1 的链接。
tsx
import Link from 'next/link';
export default function Page() {
return (
<div>
<h1>图片列表</h1>
<Link href="/photo/1">
<img src="/img1.jpg" alt="Photo 1" width={200} />
</Link>
</div>
);
}
C. 拦截路由页面 (app/@modal/(.)photo/[id]/page.tsx)
这是软导航时看到的内容(即模态框)。注意文件夹名 (.)photo 表示拦截同级的 photo 路由。
tsx
import { Modal } from '@/components/Modal'; // 假设你有一个 Modal 组件
export default function PhotoModal({ params }: { params: { id: string } }) {
return (
<Modal>
<h1>这是拦截后的弹窗视图 (ID: {params.id})</h1>
<p>背景依然是主页</p>
</Modal>
);
}
D. 常规路由页面 (app/photo/[id]/page.tsx)
这是硬导航(刷新/直接访问)时看到的内容。
tsx
export default function PhotoPage({ params }: { params: { id: string } }) {
return (
<div className="full-page-container">
<h1>这是独立的详情页面 (ID: {params.id})</h1>
<p>没有背景列表,是一个完整的页面。</p>
</div>
);
}
E. 默认处理 (app/@modal/default.tsx)
当不在拦截状态时(比如用户在主页),@modal 插槽需要渲染点什么。通常返回 null。
tsx
export default function Default() {
return null;
}
总结:为什么要用它?
- URL 可分享性 (Shareable URLs): 即使是弹窗,URL 也会更新。用户复制链接发给别人,别人能看到对应的内容(虽然是以独立页面的形式),解决了传统 SPA 弹窗无法分享具体内容的问题。
- 上下文保留 (Context Preservation): 用户不需要离开当前的列表页就能查看详情,关闭弹窗后可以继续刚才的浏览位置,体验非常流畅。
- 渐进式增强: 既拥有 SPA 的流畅体验,又保留了传统网页的深层链接能力。
注意事项
- 路径层级:
(..)是基于路由片段(Route Segments)的,而不是严格的文件系统文件夹。如果你使用了 Route Groups(如(shop)),计算层级时可能会让你感到困惑,需要仔细核对。 - 必须配合 Link 组件: 拦截路由只有通过 Next.js 的
<Link>组件或router.push()导航时才会触发。浏览器地址栏直接输入是不会触发拦截的。