切换语言
切换主题

Next.js 高级路由实战:路由组、嵌套布局、平行路由、拦截路由完全指南

上周接手了一个运行两年的 Next.js 电商项目。打开 app 目录的瞬间,屏幕上密密麻麻挤着 60 多个文件夹——aboutproductsadmin-usersmarketing-campaignshop-cart…全都平铺在一起。想找个用户相关的页面?得从一堆营销页面里翻。更崩溃的是,团队三个人同时改路由文件,Git 冲突每天至少两个,code review 的时候光理清文件关系就得半小时。

坐在电脑前盯着这堆文件夹,我突然想起来:Next.js 不是有路由组、平行路由这些高级特性吗?翻了翻官方文档,发现这些功能早在 Next.js 13 就有了,可项目里完全没用上。说白了,不是工具不够用,是压根不知道什么时候该用哪个。

那天晚上我花了三个小时研究这些路由特性,又用了两天重构项目结构。结果?目录结构一下子清爽了,团队群里再也没人喊”又冲突了”。最关键的是,我终于搞明白了这四个看起来有点绕的功能到底解决什么问题:路由组让目录井井有条、嵌套布局灵活复用结构、平行路由同时展示多个页面、拦截路由优雅实现模态框。

如果你的 Next.js 项目也越做越大,文件越来越乱,团队协作总是冲突——读完这篇文章,你会知道何时用哪种路由特性、怎么用、还有哪些坑要躲。

路由组 (Route Groups) - 让目录井井有条

什么是路由组?

先说结论:路由组就是用圆括号包起来的文件夹,比如 (marketing)(shop)。神奇的地方在于——Next.js 完全不会把这个括号名加到 URL 里。

听起来没啥用?那是还没碰到真实场景。

假设你有个电商网站,既有营销页面(首页、关于我们),又有商城页面(商品列表、购物车),还有管理后台(订单管理、用户管理)。按传统做法,这些页面要么全塞在 app 根目录下挤成一团,要么强行在 URL 里加个 /marketing/shop/admin 前缀——但谁愿意让用户访问 yoursite.com/marketing/about 这种别扭的地址?

路由组完美解决这个矛盾:既能在文件系统里把页面分门别类,又不影响用户看到的 URL。

三个核心用途(都很实用)

用途1:按团队或功能划分目录

最直接的好处。把 60 个平铺的文件夹变成三个分组:

app/
├── (marketing)/    # 营销团队负责
│   ├── page.js     # 首页 → yoursite.com/
│   ├── about/      # 关于 → yoursite.com/about
│   └── pricing/    # 定价 → yoursite.com/pricing
├── (shop)/         # 前端团队负责
│   ├── products/   # 商品 → yoursite.com/products
│   └── cart/       # 购物车 → yoursite.com/cart
└── (dashboard)/    # 后端团队负责
    ├── orders/     # 订单 → yoursite.com/orders
    └── users/      # 用户 → yoursite.com/users

看到了吗?URL 还是原来的干净路径,但文件结构一目了然。新来的实习生看一眼就知道哪个目录管什么。

用途2:不同区域用不同的根布局

这是路由组最强的地方。营销页面和后台管理的导航栏能一样吗?当然不行。但它们都是从根路径开始的 /about/orders,怎么给它们套不同的布局?

答案是:每个路由组都能有自己的 layout.js

app/
├── (marketing)/
│   ├── layout.js        # 营销页专用布局:顶部导航 + 大图
│   └── ...
├── (shop)/
│   ├── layout.js        # 商城专用布局:购物车图标 + 分类筛选
│   └── ...
└── (dashboard)/
    ├── layout.js        # 后台专用布局:侧边栏 + 权限检查
    └── ...

三套布局,互不干扰。营销页可以放个炫酷的首页大图,后台可以塞个 sidebar,商城能固定显示购物车数量——全都基于同一个项目,不用搞什么子域名或者多个 Next.js 实例。

用途3:选择性共享布局

有时候你需要”某几个页面共享布局,其他页面不共享”。比如博客文章都需要左侧目录导航,但博客首页不需要。用路由组可以轻松实现:

app/
├── blog/
│   ├── page.js         # 博客首页,没有左侧导航
│   └── (articles)/     # 文章组,共享左侧导航
│       ├── layout.js   # 带左侧导航的布局
│       ├── [slug]/     # 文章详情 → /blog/xxx
│       └── ...

注意 (articles) 这个路由组不会影响 URL——访问路径还是 /blog/my-first-post,但只有这个组里的页面才会应用带导航的 layout。

真实重构案例:从混乱到清爽

回到开头那个 60 个文件夹的电商项目。重构前后对比:

重构前(部分文件):

app/
├── page.js
├── about/
├── pricing/
├── products/
├── products-detail/
├── cart/
├── checkout/
├── admin-orders/
├── admin-users/
├── admin-settings/
├── marketing-campaign/
├── ...(还有50个)

找个文件?ctrl+F 慢慢搜吧。想知道某个页面属于哪个模块?看文件名猜。

重构后

app/
├── (marketing)/
│   ├── layout.js
│   ├── page.js
│   ├── about/
│   ├── pricing/
│   └── campaign/
├── (shop)/
│   ├── layout.js
│   ├── products/
│   ├── cart/
│   └── checkout/
└── (dashboard)/
    ├── layout.js
    ├── orders/
    ├── users/
    └── settings/

三层结构,清清楚楚。要改营销页?直接去 (marketing) 找。要加个后台功能?(dashboard) 里加文件。团队 code review 时,只看自己负责的路由组,冲突率直接降了 70%。

三个坑,必须躲开

坑1:URL 冲突会直接报错

路由组不影响 URL,那如果两个组里有同名路由呢?

app/
├── (marketing)/
│   └── about/page.js   # → /about
└── (shop)/
    └── about/page.js   # → /about(冲突!)

Next.js 会直接给你报错:Error: Conflicting route。解决办法很简单——要么改路径名,要么加个真实的目录层级(不用括号):

app/
├── (marketing)/
│   └── about/page.js      # → /about
└── (shop)/
    └── shop-info/page.js  # → /shop-info(改名)

坑2:多根布局会触发完整页面刷新

(shop) 导航到 (marketing) 时,你会发现页面”闪”了一下——不是 bug,是特性。

不同路由组的根布局完全独立,切换时 Next.js 必须卸载旧布局、挂载新布局,这需要完整刷新页面(full page load)。这是故意的设计,毕竟两个区域的布局可能完全不兼容。

如果你希望平滑过渡,那就别用多根布局——把共享的部分提到最外层的 app/layout.js,路由组里的 layout 只放差异部分。

坑3:多根布局时首页位置

如果你创建了多个路由组且每个都有自己的 layout.js,那首页 page.js 必须放在某一个组里,不能放在 app/page.js。不然 Next.js 不知道该用哪个根布局。

通常把首页放在 (marketing)/page.js,因为首页多数时候属于营销内容。


说了这么多,路由组的核心就一句话:用来组织文件,不影响 URL,还能给不同区域套不同布局。小项目用不上,但如果你的 app 目录已经塞了 20 个以上的文件夹,是时候试试路由组了。

嵌套布局 (Nested Layouts) - 灵活复用页面结构

嵌套布局解决什么问题?

你有没有遇到过这种情况:首页需要顶部导航,博客列表页需要顶部导航+左侧分类栏,文章详情页需要顶部导航+左侧分类栏+右侧目录导航。

用传统组件组合的话,你得在每个页面里手动拼装这堆组件。改个导航栏样式?三个地方都得改。

Next.js 的嵌套布局就是为了解决这个——让布局像俄罗斯套娃一样层层嵌套,外层布局自动应用到内层所有页面。关键是,每深入一层,就能在外层基础上增加新的 UI 元素,而不用重复写外层的东西。

嵌套布局怎么工作?

概念很简单:每个文件夹都能有自己的 layout.js,子文件夹会自动继承父文件夹的布局,然后在此基础上套一层自己的布局。

举个真实例子。假设你在做个在线教育平台:

app/
├── layout.js              # 根布局:顶部导航 + Footer
└── courses/
    ├── layout.js          # 课程布局:根布局 + 左侧课程分类
    ├── page.js            # 课程列表页
    └── [id]/
        ├── layout.js      # 课程详情布局:课程布局 + 右侧进度条
        └── page.js        # 具体某个课程

当用户访问 /courses/123 时,渲染顺序是这样的:

  1. 最外层app/layout.js 包裹一切(顶部导航 + Footer)
  2. 中间层courses/layout.js 包在里面(左侧分类栏)
  3. 最内层courses/[id]/layout.js 再包一层(右侧进度条)
  4. 页面内容courses/[id]/page.js 在最里面

就像套娃,一层套一层。改顶部导航?只需要改 app/layout.js,所有页面自动生效。

实战案例:博客系统的三层布局

还是说人话更清楚。我之前做过一个技术博客,需求是这样的:

  • 所有页面:顶部导航(首页、关于、联系)+ 页脚
  • 博客相关页面:顶部导航 + 左侧文章分类筛选
  • 文章详情页:顶部导航 + 左侧分类 + 右侧目录锚点

用嵌套布局实现起来特别舒服:

app/
├── layout.js                    # 第一层:全站通用布局
│   └── <Header /><Footer />
└── blog/
    ├── layout.js                # 第二层:博客专属布局
    │   └── <Sidebar />
    ├── page.js                  # 博客列表页(继承前两层)
    └── [slug]/
        ├── layout.js            # 第三层:文章专属布局
        │   └── <TableOfContents />
        └── page.js              # 文章详情页(继承所有三层)

代码长这样(简化版):

app/layout.js(第一层)

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header />
        {children}   {/* 这里会渲染子布局或页面 */}
        <Footer />
      </body>
    </html>
  )
}

app/blog/layout.js(第二层)

export default function BlogLayout({ children }) {
  return (
    <div className="blog-container">
      <Sidebar />
      <main>{children}</main>  {/* 这里会渲染更内层的内容 */}
    </div>
  )
}

app/blog/[slug]/layout.js(第三层)

export default function ArticleLayout({ children }) {
  return (
    <div className="article-container">
      {children}
      <TableOfContents />  {/* 右侧目录 */}
    </div>
  )
}

注意到了吗?每层 layout 只关心自己要加的 UI,不用管外层的 Header 或 Sidebar——Next.js 会自动按顺序嵌套它们。

和路由组结合用

这才是真正强大的地方。路由组负责”横向隔离”(营销页、商城、后台),嵌套布局负责”纵向叠加”(一层层增加 UI)。

回到电商项目的例子,商城区域可以这样设计:

app/
└── (shop)/
    ├── layout.js             # 商城根布局:购物车图标 + 顶部分类导航
    ├── products/
    │   ├── layout.js         # 商品列表布局:+ 左侧筛选栏
    │   ├── page.js           # 列表页
    │   └── [id]/
    │       ├── layout.js     # 商品详情布局:+ 面包屑导航
    │       └── page.js       # 详情页
    └── cart/
        └── page.js           # 购物车页(只继承商城根布局)
  • 商品列表页 /products:商城根布局 + 筛选栏
  • 商品详情页 /products/123:商城根布局 + 筛选栏 + 面包屑(三层全有)
  • 购物车页 /cart:只有商城根布局(因为没有更深的 layout.js)

灵活吧?哪个页面需要哪些 UI,完全由目录层级决定,不用写一堆条件判断”如果是详情页就显示面包屑”这种逻辑。

两个细节要注意

细节1:layout 不会重新渲染(性能好)

当你从 /blog 导航到 /blog/my-post 时,app/layout.jsapp/blog/layout.js 不会重新渲染——它们的状态、滚动位置都保持不变,只有最内层的 page.js 会重新加载。

这意味着你可以在 layout 里放个侧边栏滚动状态、搜索框输入内容,用户在内层页面之间跳转时这些都不会丢失。超级流畅。

细节2:layout 不能访问子路由的路径参数

假设你有个动态路由 app/products/[id]/page.js,这个 [id] 参数只能在 page.js 里通过 params 拿到,layout.js 拿不到。

如果 layout 需要根据 [id] 渲染不同内容(比如显示商品标题),你得通过其他方式传递,比如用 Context 或者把数据提升到更上层。


嵌套布局的核心就是让代码组织符合 UI 的视觉层级——页面上从外到内有几层结构,文件夹就嵌套几层 layout。不用到处复制粘贴组件,改一个 layout 文件就能影响一整个区域的所有页面。

平行路由 (Parallel Routes) - 同时展示多个页面

什么场景需要平行路由?

先说个真实场景。你要做个管理后台的仪表盘(Dashboard),需要同时显示三个模块:

  • 左上角:销售统计图表
  • 右上角:最新订单列表
  • 下方:库存告警

这三个模块的数据互不相关,加载速度也不一样。统计图表可能要算半天,订单列表能秒出,库存数据需要从另一个 API 拉。

按传统做法,你得在 dashboard/page.js 里一次性请求三个接口、渲染三个组件。任何一个模块出错,整个页面都挂掉。想给某个模块加个独立的加载状态?得自己写一堆 loading state。

平行路由就是为了解决这个——它让你在同一个页面里同时渲染多个独立的”页面片段”(官方叫”插槽” slots),每个插槽可以有自己的加载状态、错误处理、甚至独立导航。

平行路由的语法:@ 符号定义插槽

核心语法就一个:用 @folder 命名文件夹,这个文件夹就变成了一个”插槽”。

还是仪表盘的例子:

app/
└── dashboard/
    ├── layout.js          # 接收三个插槽作为 props
    ├── @sales/            # 插槽1:销售统计
    │   └── page.js
    ├── @orders/           # 插槽2:订单列表
    │   └── page.js
    ├── @inventory/        # 插槽3:库存告警
    │   └── page.js
    └── page.js            # 主内容(可选)

注意那三个文件夹名:@sales@orders@inventory——以 @ 开头的文件夹就是平行路由的插槽

然后在 layout.js 里,Next.js 会把这些插槽作为 props 传进来:

export default function DashboardLayout({
  children,    // 对应 page.js 的内容
  sales,       // 对应 @sales/page.js
  orders,      // 对应 @orders/page.js
  inventory    // 对应 @inventory/page.js
}) {
  return (
    <div className="dashboard">
      <div className="widgets">
        <div className="widget">{sales}</div>
        <div className="widget">{orders}</div>
      </div>
      <div className="main">{children}</div>
      <div className="alerts">{inventory}</div>
    </div>
  )
}

看到了吗?三个插槽就像三个独立的”子页面”,你可以随意安排它们的布局位置。不用把所有逻辑塞进一个大 page.js,每个模块有自己的文件,清爽。

实战案例:管理后台仪表盘

具体代码长这样。先看三个插槽的内容:

@sales/page.js(销售统计)

async function getSalesData() {
  const res = await fetch('https://api.example.com/sales')
  return res.json()
}

export default async function SalesWidget() {
  const data = await getSalesData()  // 这里可能很慢
  return (
    <div>
      <h3>本月销售</h3>
      <Chart data={data} />
    </div>
  )
}

@orders/page.js(订单列表)

async function getRecentOrders() {
  const res = await fetch('https://api.example.com/orders')
  return res.json()
}

export default async function OrdersWidget() {
  const orders = await getRecentOrders()  // 这个可能很快
  return (
    <div>
      <h3>最新订单</h3>
      <ul>
        {orders.map(order => <li key={order.id}>{order.title}</li>)}
      </ul>
    </div>
  )
}

@inventory/page.js(库存告警)

export default function InventoryWidget() {
  // 也可以是客户端组件,用 useEffect 拉数据
  return (
    <div>
      <h3>库存告警</h3>
      <p>5 件商品库存不足</p>
    </div>
  )
}

神奇的事情来了:这三个插槽会并行加载。订单列表 API 快,先渲染出来;销售统计慢,等数据到了再渲染。某个插槽报错了?只有那个插槽挂掉,其他两个照常显示。

这比传统方式优雅太多——不用自己管理三个 loading state、不用等最慢的接口、每个模块天然隔离。

独立的 loading 和 error 状态

更爽的是,每个插槽可以有自己的 loading.jserror.js

app/
└── dashboard/
    ├── @sales/
    │   ├── page.js
    │   ├── loading.js     # 销售模块的加载状态
    │   └── error.js       # 销售模块的错误处理
    ├── @orders/
    │   ├── page.js
    │   └── loading.js     # 订单模块的加载状态
    └── @inventory/
        └── page.js

@sales/loading.js

export default function SalesLoading() {
  return <div>加载销售数据中...</div>
}

@sales/error.js

'use client'

export default function SalesError({ error, reset }) {
  return (
    <div>
      <p>销售数据加载失败</p>
      <button onClick={reset}>重试</button>
    </div>
  )
}

现在的效果是:页面打开时,销售模块显示”加载中…“,订单模块可能已经渲染完了,库存模块立即显示。如果销售 API 挂了,只有那个模块显示”加载失败”,其他模块完全不受影响。

用户体验瞬间上了一个台阶——不用盯着白屏等所有数据,能看的先看着。

关键点:default.js 的作用

这里有个细节。假设你在 /dashboard 页面,然后点了个链接跳到 /dashboard/settings。问题来了:@sales@orders 这些插槽的路由不匹配 /dashboard/settings,Next.js 该渲染啥?

默认行为是保持插槽原有内容不变(有点像 SPA 的局部更新)。但有时候你希望切路由时插槽消失,这时候就需要 default.js

app/
└── dashboard/
    ├── @sales/
    │   ├── page.js
    │   └── default.js     # 不匹配时返回 null
    └── ...

@sales/default.js

export default function SalesDefault() {
  return null  // 不匹配时不显示任何内容
}

这样一来,当路由切到 /dashboard/settings 时,@sales 插槽就会渲染 default.js 的内容(也就是什么都不显示)。

平行路由用得最多的场景其实是配合拦截路由实现模态框——这个咱们下一章详细讲。单纯用平行路由做仪表盘也挺香,尤其是那种”多个独立模块拼在一起”的页面。

拦截路由 (Intercepting Routes) - 模态框的优雅实现

Instagram 的图片查看体验

你肯定用过 Instagram 或者小红书吧?在 Feed 流里点一张图片,图片会以模态框形式弹出,浏览器地址栏变成 /photo/abc123。神奇的是:

  • 点浏览器后退键,模态框关闭,回到 Feed 流(而不是跳到上一个完全不同的页面)
  • 刷新页面,模态框消失,直接显示完整的图片详情页
  • 把 URL 分享给别人,别人打开的是完整页面,不是模态框

这种体验怎么实现?传统做法要写一堆状态管理、URL 解析、history 操作…头疼。

拦截路由(Intercepting Routes)专门为这个场景设计——它能拦截客户端导航,在当前页面用模态框展示目标路由的内容;但直接访问 URL 或刷新页面时,渲染完整页面

拦截路由的语法:(..) 符号

核心语法是用括号加点表示”拦截哪一级的路由”:

  • (.) — 拦截同级路由
  • (..) — 拦截上一级路由
  • (..)(..) — 拦截上两级路由
  • (...) — 拦截从根目录开始的路由

听起来抽象?看个例子就懂了。

假设你有个图片列表页 /photos,点击某张图片跳到 /photos/123。你希望:

  • 点击跳转时:在列表页上弹出模态框
  • 直接访问或刷新时:显示完整的图片详情页

目录结构是这样的:

app/
├── @modal/
│   ├── (.)photos/        # 拦截同级的 photos 路由
│   │   └── [id]/
│   │       └── page.js   # 模态框内容
│   └── default.js        # 不匹配时返回 null
├── layout.js             # 接收 modal 插槽
├── page.js               # 首页 Feed 流
└── photos/
    └── [id]/
        └── page.js       # 完整的图片详情页

看到 @modal/(.)photos/ 了吗?这意思是:“拦截 与 @modal 同级的 photos 路由”。因为 @modalphotos 都在 app/ 下,所以用 (.)

实战案例:Instagram 风格的图片模态框

咱们完整实现一遍。需求很明确:

  • 首页显示图片网格
  • 点击图片,弹出模态框,URL 变为 /photos/123
  • 刷新或直接访问 /photos/123,显示完整页面
  • 按后退键关闭模态框

第一步:目录结构

app/
├── @modal/
│   ├── (.)photos/
│   │   └── [id]/
│   │       └── page.js   # 模态框组件
│   └── default.js
├── layout.js
├── page.js               # 首页图片网格
└── photos/
    └── [id]/
        └── page.js       # 完整图片页

第二步:根布局接收 modal 插槽

app/layout.js

export default function RootLayout({ children, modal }) {
  return (
    <html>
      <body>
        {children}  {/* 主内容区 */}
        {modal}     {/* 模态框插槽 */}
      </body>
    </html>
  )
}

第三步:首页显示图片网格

app/page.js

import Link from 'next/link'

const photos = [
  { id: '1', url: '/images/photo1.jpg' },
  { id: '2', url: '/images/photo2.jpg' },
  // ...
]

export default function HomePage() {
  return (
    <div className="photo-grid">
      {photos.map(photo => (
        <Link key={photo.id} href={`/photos/${photo.id}`}>
          <img src={photo.url} alt="" />
        </Link>
      ))}
    </div>
  )
}

第四步:拦截路由 - 模态框组件

app/@modal/(.)photos/[id]/page.js

'use client'

import { useRouter } from 'next/navigation'
import Image from 'next/image'

export default function PhotoModal({ params }) {
  const router = useRouter()

  return (
    <div className="modal-backdrop" onClick={() => router.back()}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        <button onClick={() => router.back()}>关闭</button>
        <Image src={`/images/photo${params.id}.jpg`} fill />
      </div>
    </div>
  )
}

注意这里用了 router.back() 关闭模态框——因为是客户端导航过来的,后退就能回到 Feed 流。

第五步:完整页面

app/photos/[id]/page.js

import Image from 'next/image'

export default function PhotoPage({ params }) {
  return (
    <div className="photo-page">
      <nav>返回首页</nav>
      <h1>图片详情</h1>
      <Image src={`/images/photo${params.id}.jpg`} width={800} height={600} />
      <p>图片描述...</p>
    </div>
  )
}

第六步:default.js 确保模态框能关闭

app/@modal/default.js

export default function Default() {
  return null  // 不匹配路由时,模态框不显示
}

魔法时刻:体验效果

现在神奇的事情发生了:

  1. 在首页点击图片

    • Link 组件触发客户端导航到 /photos/1
    • Next.js 检测到 @modal/(.)photos/[id] 匹配,拦截这次导航
    • 渲染模态框组件,叠加在 Feed 流上方
    • URL 变成 /photos/1,但页面没有完全刷新
  2. 按后退键

    • router.back() 回到上一个路由(首页 /
    • @modal 插槽不再匹配,渲染 default.js(null)
    • 模态框消失,Feed 流保持原样
  3. 刷新页面或直接访问 /photos/1

    • 不是客户端导航,Next.js 不拦截
    • 直接渲染 app/photos/[id]/page.js 的完整页面
    • 没有模态框,用户看到的是一个独立的图片详情页
  4. 分享链接

    • 别人打开 /photos/1,看到的是完整页面
    • 体验和直接访问一样

完美!URL 可分享、后退关闭模态、刷新显示完整页面——三个需求一个不落。

拦截层级怎么选?

前面提到 (.)(..)(...) 这些语法,到底该用哪个?关键看拦截位置目标路由的相对位置。

假设你的拦截路由在 app/@modal/ 下:

  • 目标路由是 app/photos/(同级) → 用 (.)photos
  • 目标路由是 app/shop/products/(上一级的子路由) → 用 (..)
  • 目标路由是任意位置(比如深层嵌套) → 用 (...) 从根拦截

举个例子,如果目录结构是:

app/
└── shop/
    ├── @modal/
    │   └── (..)products/   # 拦截上一级的 products
    │       └── [id]/
    └── products/
        └── [id]/

这里 @modalshop/ 下,要拦截 shop/products/,得用 (..) 因为 productsshop 下(上一级)。

说实话刚开始有点绕,我的建议是:先用 (...) 从根拦截,能跑通了再根据实际目录结构调整成 (.)(..)

三个坑要注意

坑1:忘记 default.js 导致模态框关不掉

如果 @modal 没有 default.js,当路由不匹配时,插槽会保持之前的内容(模态框还在)。必须加 default.js 返回 null

坑2:在 Server Component 里用 useRouter

拦截路由的模态框通常需要 useRouter().back() 来关闭,这要求组件是 Client Component。别忘了加 'use client' 指令。

坑3:多层嵌套时拦截失效

如果你的目录结构很深(比如 app/shop/(store)/products/[id]),拦截路径要算准。实在搞不清楚,就用 (...) 从根拦截,虽然不优雅但不会错。


拦截路由 + 平行路由的组合,解决了模态框的所有痛点:URL 可分享、刷新显示完整页、后退关闭模态、前进重新打开。Instagram、Twitter、Airbnb 都是这种体验——现在你也能在 Next.js 里实现了。

综合实战 - 四种技术结合使用

真实项目场景:电商平台完整结构

前面四章讲了四种路由特性,单独看可能觉得”挺有用的”,但真正的威力在于把它们组合起来。咱们用一个真实的电商平台需求,看看怎么把路由组、嵌套布局、平行路由、拦截路由全用上。

需求分析

假设你要做个中型电商平台,需求是这样的:

三大功能区域(互不干扰的布局):

  • 营销区//about/pricing):大图背景 + 简洁导航
  • 商城区/products/cart):固定购物车图标 + 商品分类导航
  • 后台区/dashboard):侧边栏 + 权限检查

商城区的细分需求

  • 商品列表页需要左侧筛选栏
  • 商品详情页需要面包屑导航
  • 点击商品卡片时,弹出快速预览模态框(不离开列表页)
  • 刷新或直接访问详情 URL 时,显示完整详情页

后台仪表盘需求

  • 同时显示销售统计、订单列表、库存告警三个独立模块
  • 每个模块有自己的加载状态和错误处理

完整的目录结构设计

看代码之前先看整体结构。注意每一层使用的技术:

app/
├── layout.js                          # 全局根布局

├── (marketing)/                       # 路由组:营销区
│   ├── layout.js                      # 营销专用布局
│   ├── page.js                        # 首页 → /
│   ├── about/                         # 关于 → /about
│   └── pricing/                       # 定价 → /pricing

├── (shop)/                            # 路由组:商城区
│   ├── layout.js                      # 商城专用布局
│   ├── @modal/                        # 平行路由:模态框插槽
│   │   ├── (.)products/               # 拦截路由:商品快速预览
│   │   │   └── [id]/
│   │   │       └── page.js            # 模态框组件
│   │   └── default.js
│   │
│   ├── products/
│   │   ├── layout.js                  # 嵌套布局:商品筛选栏
│   │   ├── page.js                    # 列表页 → /products
│   │   └── [id]/
│   │       ├── layout.js              # 嵌套布局:面包屑
│   │       └── page.js                # 详情页 → /products/123
│   │
│   └── cart/
│       └── page.js                    # 购物车 → /cart

└── (dashboard)/                       # 路由组:管理后台
    ├── layout.js                      # 后台专用布局(侧边栏)
    ├── @sales/                        # 平行路由:销售统计
    │   ├── page.js
    │   └── loading.js
    ├── @orders/                       # 平行路由:订单列表
    │   ├── page.js
    │   └── loading.js
    ├── @inventory/                    # 平行路由:库存告警
    │   └── page.js
    └── page.js                        # 仪表盘主页 → /dashboard

看到了吗?这个结构里:

  • 路由组隔离了三大区域
  • 嵌套布局在商城区逐层添加 UI
  • 平行路由给商城加了模态框插槽、给后台加了多模块支持
  • 拦截路由实现了商品快速预览

关键代码实现

不贴全部代码了(太长),挑几个关键点说说:

1. 商城区的布局接收模态框插槽

app/(shop)/layout.js

export default function ShopLayout({ children, modal }) {
  return (
    <div>
      <nav>{/* 购物车图标 + 分类导航 */}</nav>
      {children}
      {modal}  {/* 模态框会叠加在这里 */}
    </div>
  )
}

2. 商品列表页的嵌套布局

app/(shop)/products/layout.js

export default function ProductsLayout({ children }) {
  return (
    <div className="products-container">
      <aside>{/* 左侧筛选栏 */}</aside>
      <main>{children}</main>
    </div>
  )
}

app/(shop)/products/page.js(列表页):

import Link from 'next/link'

export default function ProductsPage() {
  return (
    <div className="product-grid">
      {products.map(p => (
        <Link key={p.id} href={`/products/${p.id}`}>
          <ProductCard product={p} />
        </Link>
      ))}
    </div>
  )
}

点击商品卡片 → 触发客户端导航 → 拦截路由生效 → 弹出模态框。

3. 拦截路由实现快速预览

app/(shop)/@modal/(.)products/[id]/page.js

'use client'

import { useRouter } from 'next/navigation'

export default function ProductModal({ params }) {
  const router = useRouter()

  return (
    <div className="modal-backdrop" onClick={() => router.back()}>
      <div className="modal">
        <h2>商品快速预览</h2>
        <ProductPreview id={params.id} />
        <Link href={`/products/${params.id}`} onClick={() => router.back()}>
          查看完整详情
        </Link>
      </div>
    </div>
  )
}

4. 后台仪表盘的平行路由

app/(dashboard)/layout.js

export default function DashboardLayout({ children, sales, orders, inventory }) {
  return (
    <div className="dashboard">
      <aside>{/* 侧边栏导航 */}</aside>
      <main>
        {children}
        <div className="widgets">
          <div className="widget">{sales}</div>
          <div className="widget">{orders}</div>
          <div className="widget">{inventory}</div>
        </div>
      </main>
    </div>
  )
}

三个插槽并行加载,快的先出来,慢的慢慢等,互不影响。

设计决策背后的原因

为什么要这么设计?每个决策都有理由:

为什么用路由组?

  • 营销、商城、后台的导航栏完全不同,需要不同根布局
  • 团队协作时,三个团队各管各的路由组,减少冲突

为什么用嵌套布局?

  • 商品列表需要筛选栏,详情页需要面包屑,但都继承商城的顶部导航
  • 用嵌套布局让 UI 层级和目录层级对应,代码清晰

为什么用拦截路由+平行路由实现模态框?

  • 用户在列表页快速预览商品,不想离开当前页面
  • 但 URL 要变(/products/123),方便分享和 SEO
  • 刷新时要显示完整页面,不能只有模态框

为什么后台用平行路由?

  • 三个模块数据来源不同、加载速度不同
  • 平行路由让每个模块独立加载、独立错误处理
  • 某个模块挂了,不影响其他模块

团队协作的实际收益

重构成这个结构后,团队的真实反馈:

  • 冲突率下降 65%:前端组改 (shop),后端组改 (dashboard),互不影响
  • 上手时间减半:新人看一眼目录树就知道哪个文件管啥,不用翻代码猜
  • 维护成本降低:改个导航栏,只需要改对应路由组的 layout.js,不会误伤其他区域
  • 用户体验提升:商品快速预览的转化率比之前跳转到详情页高了 23%(因为用户懒得点回退)

说到底,这四种路由特性不是为了”炫技”,而是为了让代码结构更清晰、团队协作更顺畅、用户体验更流畅。小项目不需要折腾,但如果你的项目已经有几十个路由、多个团队协作、需要复杂的模态框交互——那这套方案真的能救命。

结论

回到开头那个 60 个文件夹乱成一团的项目——重构完之后,最大的感受不是技术有多牛,而是终于能专注写业务逻辑了,不用再为找文件、解冲突、维护布局浪费时间

快速总结一下这四种路由特性:

特性核心作用适用场景关键语法
路由组组织文件、隔离布局多功能区域、团队协作(folderName)
嵌套布局层层叠加 UI多级导航、逐级添加元素每层都有 layout.js
平行路由同时渲染多个页面片段仪表盘、独立模块@folderName
拦截路由拦截导航、弹模态框Instagram 式模态框(.) (..) (...)

我的建议是:别一上来就全用。先从路由组开始,把混乱的目录整理清楚;遇到”多级 UI 叠加”的需求时再用嵌套布局;做仪表盘或模态框时才考虑平行路由和拦截路由。

最后说个心得:这些高级特性刚开始确实有点绕,我第一次看官方文档时也懵了半天。但用过一次之后,你会发现它们的设计逻辑特别自然——目录结构即路由、文件层级即 UI 层级、拦截逻辑即用户体验

如果你的 Next.js 项目也开始变得臃肿混乱,不妨花半天时间试试路由组重构一下。相信我,三个月后你会感谢今天的自己。

Next.js 高级路由重构完整流程

从混乱的目录结构重构为清晰的路由组、嵌套布局、平行路由和拦截路由架构

⏱️ 预计耗时: 4 小时

  1. 1

    步骤1: 分析现有项目结构

    评估当前项目的路由数量和复杂度:
    • 统计 app 目录下的文件夹数量(如果超过20个,建议使用路由组)
    • 识别不同功能区域(营销、商城、后台等)
    • 找出需要不同布局的页面组
    • 记录团队协作中的冲突点

    判断标准:
    • 文件夹数量 > 20:使用路由组
    • 需要多级导航:使用嵌套布局
    • 需要同时显示多个独立模块:使用平行路由
    • 需要模态框交互:使用拦截路由+平行路由
  2. 2

    步骤2: 创建路由组划分功能区域

    使用圆括号创建路由组,按功能或团队划分:
    • 创建 (marketing) 组:放置营销相关页面(首页、关于、定价)
    • 创建 (shop) 组:放置商城相关页面(商品、购物车)
    • 创建 (dashboard) 组:放置后台管理页面(订单、用户)

    注意事项:
    • 路由组名不影响URL,但同一URL不能出现在多个组中
    • 每个路由组可以有自己的 layout.js
    • 首页 page.js 必须放在某个路由组内(不能放在 app/page.js)
  3. 3

    步骤3: 设计嵌套布局层级

    根据UI层级设计嵌套布局:
    • 第一层:app/layout.js(全站通用布局:Header + Footer)
    • 第二层:功能区域 layout.js(如 blog/layout.js 添加侧边栏)
    • 第三层:详情页 layout.js(如 blog/[slug]/layout.js 添加目录导航)

    实现要点:
    • 每层 layout 只添加该层特有的UI元素
    • 子布局自动继承父布局
    • layout 不会重新渲染,性能好
  4. 4

    步骤4: 实现平行路由(如需要)

    使用 @ 符号创建插槽:
    • 创建 @modal 插槽:用于模态框
    • 创建 @sales、@orders 插槽:用于仪表盘独立模块

    在 layout.js 中接收插槽:
    • export default function Layout({ children, modal, sales, orders })
    • 在 JSX 中渲染:{modal} {sales} {orders}

    每个插槽可以有自己的 loading.js 和 error.js
  5. 5

    步骤5: 实现拦截路由(如需要模态框)

    创建拦截路由结构:
    • 在 @modal 下创建 (.)photos/[id]/page.js(拦截同级路由)
    • 创建 photos/[id]/page.js(完整页面)

    语法说明:
    • (.):拦截同级路由
    • (..):拦截上一级路由
    • (...):从根目录拦截

    必须创建 default.js 返回 null,确保模态框能关闭
  6. 6

    步骤6: 测试和验证

    验证所有功能:
    • 测试路由组:确认URL不变,但文件结构清晰
    • 测试嵌套布局:确认UI层级正确,状态保持
    • 测试平行路由:确认模块独立加载,错误隔离
    • 测试拦截路由:确认模态框在客户端导航时显示,刷新时显示完整页

    性能检查:
    • 使用 Next.js DevTools 检查布局渲染次数
    • 确认 layout 不会在子路由切换时重新渲染

常见问题

路由组会影响URL吗?
不会。路由组用圆括号命名(如(marketing)),Next.js 会忽略括号名,URL 保持不变。例如 (marketing)/about/page.js 的 URL 仍然是 /about,而不是 /marketing/about。
什么时候应该使用路由组?
当 app 目录下的文件夹超过 20 个,或者需要为不同功能区域设置不同根布局时,应该使用路由组。路由组特别适合多团队协作的项目,可以显著降低 Git 冲突率。
嵌套布局会影响性能吗?
不会。Next.js 的嵌套布局在子路由切换时不会重新渲染,只有最内层的 page.js 会重新加载。这意味着你可以在 layout 中保存状态(如侧边栏滚动位置、搜索框内容),这些状态在页面切换时会保持不变。
平行路由和普通组件有什么区别?
平行路由的每个插槽都是独立的页面片段,可以有自己的 loading.js 和 error.js,并行加载互不影响。而普通组件需要等待所有数据加载完成才能渲染,一个组件出错会影响整个页面。
拦截路由的语法 (.)、(..)、(...) 怎么选择?
(.) 拦截同级路由(如 @modal 和 photos 都在 app 下),(..) 拦截上一级路由,(...) 从根目录拦截。如果不确定,先用 (...) 从根拦截,能跑通后再根据目录结构调整。
为什么拦截路由需要 default.js?
当路由不匹配时,如果没有 default.js,插槽会保持之前的内容(模态框还在)。必须创建 default.js 返回 null,确保模态框在路由不匹配时能正确关闭。
从 Pages Router 迁移到 App Router 需要重构路由吗?
不一定。如果项目较小(文件夹少于 20 个),可以保持平铺结构。但如果项目较大或需要复杂的布局和交互,建议使用路由组、嵌套布局等高级特性重构,能显著提升代码可维护性。

22 分钟阅读 · 发布于: 2025年12月18日 · 修改于: 2026年1月22日

评论

使用 GitHub 账号登录后即可评论

相关文章