系统设计 / 路由设计 · 稳定入口与可变目标 / 并行路由与拦截路由 待审核 17 / 23

并行路由与拦截路由

嵌套路由里,一条 URL 命中一条单链:外层 layout 套内层页面,outlet 只有一格。Next.js app router 再走一步——一个 layout 可以有多个具名 outlet(slot),一条 URL 同时把它们都填好,这叫 parallel routes(并行路由)。每个 slot 是一棵独立子树,各自有自己的 loading 与 error 状态,互不阻塞。

它和 intercepting routes(拦截路由)搭起来,解一个常见需求:照片墙点开一张图,想要模态框浮在原列表上;但把那张图的链接分享出去或刷新,又该是独立整页。同一个 URL /photo/3,怎么到达的决定了渲染成什么——客户端软导航(点链接)被拦截进模态槽,硬导航(刷新、直接打开、分享链)不被拦截,走整页。

图 0-1 · 同一个 /photo/3 在两种到达方式下的渲染差异。可切换软导航与硬导航,观察 @modal 槽与 children 槽各自填了什么。

点缩略图是软导航:router 在客户端改地址,@modal 槽里的拦截路由 (.)photo/[id] 命中而浮起模态,children 槽仍是那张列表、没有卸载,背景还在。按刷新是硬导航:请求重新到服务端,拦截规则不参与,@modal 退回 default.js(空),children 槽把 /photo/3 解析成整页。分享出去的链接对方打开也是硬导航,所以拿到的是完整页面而不是别人列表上的一个浮层。

1 · 目录约定

Next.js 用目录约定表达这套路由。@modal 这种 @ 开头的文件夹是一个具名 slot,会作为同名 prop 传给父 layout(.)photo 这种 (.) 前缀是拦截标记,意思是拦截同级的 photo 路由,(..) 拦上一级、(...) 从根拦。default.js 是 slot 在「当前 URL 不该填它」时的回退,本例中它返回 null,所以硬导航时模态槽是空的。

1.1 · app router 的目录结构

app/
  layout.js                     # 父 layout: 同时接住 children 和 @modal 两个槽
  page.js                       # children 槽默认内容 = Feed (照片列表)
  photo/[id]/page.js            # 硬导航 / 直接打开 → 整页照片
  @modal/                       #  ← @ 开头 = 一个具名 slot, 作为 prop 传给 layout
    default.js                  #     回退: 不该填模态时返回 null
    (.)photo/[id]/page.js       #  ← (.) 前缀 = 拦截同级的 photo/[id]
                                #     软导航命中它 → 模态; 硬导航不参与 → 整页

1.2 · 父 layout 同时接住两个槽

// app/layout.js —— 具名 slot 作为 prop 进来, layout 决定怎么摆放它们
export default function Layout({ children, modal }) {
  return (
    <>
      {children}     {/* children 槽: 列表, 或 (硬导航时) 整页照片 */}
      {modal}        {/* @modal 槽: 软导航时是模态, 否则 default.js → null */}
    </>
  );
}

// (.) / (..) / (...) 的含义 —— 拦截「相对当前位置某一级」的路由:
//   (.)photo     拦同级的 photo
//   (..)photo    拦上一级的 photo
//   (...)photo   从 app 根拦 photo

2 · 为什么不用一个状态变量

showModal 这样的组件状态当然也能弹出模态,代价是模态不在 URL 里:不能分享、刷新即丢、浏览器后退键关不掉它。

把模态做成路由,它就有了真实地址——可被分享(对方看到整页)、可被后退键关闭(回到列表)、可被刷新还原(成整页)。并行与拦截路由本质上是让浮层也纳入路由状态,同时不丢掉「列表还在背景里」这件事,是把 URL 当作唯一事实来源的彻底做法。

软导航与硬导航的区别本身是软导航与守卫一页的主题:软导航是客户端 router 可以插手的过程,本页正是用这一点区分模态与整页。