在开发复杂 Web 应用时,你是否遇到过这样的需求?
- 在仪表盘中同时显示「用户信息」和「数据分析」两个独立模块;
- 点击导航栏的“登录”按钮弹出 Modal,但
/login也有独立页面; - 页面中嵌入一个可独立跳转的评论区,不影响主内容。
传统方案往往需要手动管理多个状态、URL 同步、刷新恢复等问题,代码复杂且容易出错。
而 Next.js App Router 提供了一个优雅的解决方案 —— 平行路由(Parallel Routes)。本文将用最简单的方式带你理解它,并通过完整可运行的代码示例,助你快速上手!
一、什么是平行路由?
平行路由允许你在同一个布局中同时渲染多个独立的内容区域,每个区域称为一个 Slot(插槽)。
- 每个 Slot 有自己的页面逻辑,互不影响;
- URL 只反映主内容(children),Slot 路径不体现在 URL 中;
- 支持软导航(客户端跳转)和硬导航(刷新/直接访问)的状态恢复。
✅ 举个例子:
在/dashboard页面,左侧@team显示团队设置,右侧@analytics显示访问数据。
点击/analytics/page-views,只有右侧更新,左侧保持不变。
二、命名规则:@slot 目录
Next.js 使用 @xxx 的目录名定义 Slot:
app/
├── @team/ ← team Slot
│ └── page.tsx
├── @analytics/ ← analytics Slot
│ └── page.tsx
└── layout.tsx ← 接收 team 和 analytics 作为 props
在 layout.tsx 中,Slot 会以 props 形式传入:
// app/layout.tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<div style={{ display: 'flex' }}>
<div style={{ flex: 1 }}>{team}</div>
<div style={{ flex: 2 }}>{children}</div>
<div style={{ flex: 1 }}>{analytics}</div>
</div>
);
}
⚠️ 注意:
children是隐式 Slot,对应app/page.tsx;- Slot 不是路由段,不会出现在 URL 中;
- 所有同级 Slot 必须同时为静态或动态路由。
三、实战:实现一个“社交平台的动态 Feed 流 ”
当然可以!下面我们用一个全新的实战例子来讲解 Next.js 的 平行路由(Parallel Routes),不再使用“登录 Modal”,而是构建一个 社交平台的动态 Feed 流 + 侧边评论面板 的场景。
🎯 场景描述
- 用户访问
/feed,看到一条条动态(Posts); - 点击某条动态的“评论”按钮,右侧弹出评论面板;
- 评论面板内容可通过 URL 分享,例如
/post/3/comments; - 直接访问
/post/3/comments时,显示完整评论页; - 刷新页面后,评论面板自动关闭,只显示 Feed;
- 浏览器前进/后退能正确控制评论面板的开闭。
这个需求结合了:
- Parallel Routes(实现独立评论区域)
- Intercepting Routes(在 Feed 中拦截评论页,以内联形式展示)
📂 项目结构
app/
├── @comments/ ← 评论 Slot
│ ├── (..)post/
│ │ └── [id]/
│ │ └── comments/
│ │ └── page.tsx ← 拦截路由:在 Feed 中以内联形式展示
│ ├── default.tsx ← 默认不显示评论面板
│ └── page.tsx ← 关闭评论面板(返回 null)
├── feed/
│ └── page.tsx ← 动态 Feed 主页
├── post/
│ └── [id]/
│ └── comments/
│ └── page.tsx ← 独立评论页面
└── layout.tsx ← 根布局,渲染 Feed + 评论面板
💡 关键点:
@comments是一个 Slot,用于承载评论内容;(..)post/[id]/comments是拦截路由,因为post在feed的上一级(同级目录),所以用(..);default.tsx确保刷新时不显示残留评论。
✍️ 代码实现
1. Feed 主页:app/feed/page.tsx
// app/feed/page.tsx
import Link from 'next/link';
export default function FeedPage() {
const posts = [
{ id: 1, content: '今天天气真好!' },
{ id: 2, content: '刚学了 Next.js 平行路由,太强大了!' },
{ id: 3, content: '求推荐好用的 UI 库' },
];
return (
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
<h1>动态 Feed</h1>
{posts.map((post) => (
<div key={post.id} style={{ padding: '16px', border: '1px solid #eee', margin: '12px 0' }}>
<p>{post.content}</p>
{/* 软导航到 /post/1/comments,触发拦截路由 */}
<Link href={`/post/${post.id}/comments`}>查看评论</Link>
</div>
))}
</div>
);
}
2. 独立评论页:app/post/[id]/comments/page.tsx
// app/post/[id]/comments/page.tsx
export default function CommentsPage({ params }: { params: { id: string } }) {
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<h2>完整评论页 - 动态 ID: {params.id}</h2>
<p>这是独立的评论页面,可直接访问或刷新。</p>
<ul>
<li>用户A:写得真好!</li>
<li>用户B:+1</li>
<li>用户C:求源码</li>
</ul>
</div>
);
}
3. 拦截路由(Feed 中的评论面板):app/@comments/(..)post/[id]/comments/page.tsx
// app/@comments/(..)post/[id]/comments/page.tsx
'use client';
import { useRouter } from 'next/navigation';
export default function InterceptedComments({ params }: { params: { id: string } }) {
const router = useRouter();
return (
<div
style={{
position: 'fixed',
top: 0,
right: 0,
width: '350px',
height: '100vh',
background: 'white',
borderLeft: '1px solid #ddd',
padding: '20px',
boxShadow: '-2px 0 10px rgba(0,0,0,0.1)',
zIndex: 1000,
overflowY: 'auto',
}}
>
<button
onClick={() => router.back()}
style={{ float: 'right', background: 'none', border: 'none', cursor: 'pointer' }}
>
×
</button>
<h3>评论面板 - 动态 ID: {params.id}</h3>
<p>这是在 Feed 中以内联方式展示的评论(通过拦截路由)。</p>
<ul>
<li>用户A:写得真好!</li>
<li>用户B:+1</li>
<li>用户C:求源码</li>
</ul>
</div>
);
}
🔍 为什么用
(..)post?
因为@comments是 Slot(不是路由段),所以/post实际在 上一级路由,用(..)拦截。
4. Slot 默认状态
// app/@comments/default.tsx
export default function CommentsDefault() {
return null; // 初始不显示评论面板
}
// app/@comments/page.tsx(用于关闭面板)
export default function CommentsPage() {
return null;
}
5. 根布局:app/layout.tsx
// app/layout.tsx
import Link from 'next/link';
export default function RootLayout({
children,
comments,
}: {
children: React.ReactNode;
comments: React.ReactNode;
}) {
return (
<html>
<body>
<nav style={{ padding: '10px', borderBottom: '1px solid #eee' }}>
<Link href="/feed">返回 Feed</Link>
</nav>
<div style={{ display: 'flex' }}>
<div style={{ flex: 1 }}>{children}</div>
{/* 评论面板 Slot */}
<div>{comments}</div>
</div>
</body>
</html>
);
}
🧪 效果验证
| 操作 | 行为 |
|---|---|
在 /feed 点击“查看评论” | URL 变为 /post/2/comments,右侧弹出评论面板 |
| 刷新页面 | 评论面板消失,显示完整 /post/2/comments 页面 |
| 浏览器后退 | 评论面板关闭,回到 /feed |
直接访问 /post/3/comments | 显示完整评论页(无 Feed) |
| 前进/后退多次 | 评论面板自动开闭,状态精准同步 |
✅ 完美实现:上下文保持 + URL 可分享 + 刷新安全 + 历史记录兼容!
✅ 总结
这个例子展示了平行路由 + 拦截路由在真实业务场景中的强大组合:
@commentsSlot:隔离评论区域,独立渲染;(..)post/[id]/comments:在 Feed 中“拦截”评论页,以内联形式展示;default.tsx:确保刷新后不残留 UI;- URL 语义清晰:
/post/2/comments既是独立页,也是 Modal 的入口。
七、总结
平行路由是 Next.js App Router 的核心高级功能,它让复杂页面的构建变得:
- ✅ 结构清晰:每个区域独立管理;
- ✅ 状态可控:软/硬导航行为明确;
- ✅ 用户体验好:支持分享、刷新、前进/后退。
如果你正在开发 Dashboard、社交 Feed、Modal 系统等,强烈推荐使用 Parallel Routes!