Menu

Parallel Routes

Parallel Routes 允许你在同一布局中同时或有条件地渲染一个或多个页面。它们对于应用中的高度动态部分非常有用,例如仪表盘和社交网站上的动态信息流。

例如,考虑一个仪表盘,你可以使用 parallel routes 同时渲染 teamanalytics 页面:

Parallel Routes Diagram

约定

插槽

Parallel routes 使用命名的插槽创建。插槽使用 @folder 约定定义。例如,以下文件结构定义了两个插槽:@analytics@team

Parallel Routes File-system Structure

插槽作为 props 传递给共享的父布局。对于上面的例子,app/layout.js 中的组件现在接受 @analytics@team 插槽 props,并可以将它们与 children prop 一起并行渲染:

app/layout.tsx
TypeScript
export default function Layout({
  children,
  team,
  analytics,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <>
      {children}
      {team}
      {analytics}
    </>
  )
}

然而,插槽不是路由段,不会影响 URL 结构。例如,对于 /@analytics/views,URL 将是 /views,因为 @analytics 是一个插槽。插槽与常规的 Page 组件结合,形成与路由段关联的最终页面。因此,你不能在同一路由段级别拥有单独的静态动态插槽。如果一个插槽是动态的,该级别的所有插槽都必须是动态的。

值得注意的是

  • children prop 是一个隐式插槽,不需要映射到文件夹。这意味着 app/page.js 等同于 app/@children/page.js

default.js

你可以定义一个 default.js 文件,在初始加载或完整页面重新加载期间,作为不匹配插槽的回退内容进行渲染。

考虑以下文件夹结构。@team 插槽有一个 /settings 页面,但 @analytics 没有。

Parallel Routes unmatched routes

当导航到 /settings 时,@team 插槽将渲染 /settings 页面,同时为 @analytics 插槽保持当前活动的页面。

在刷新时,Next.js 将为 @analytics 渲染 default.js。如果 default.js 不存在,则会渲染 404

此外,由于 children 是一个隐式插槽,当 Next.js 无法恢复父页面的活动状态时,你还需要创建一个 default.js 文件来为 children 渲染回退内容。

行为

默认情况下,Next.js 会跟踪每个插槽的活动_状态_(或子页面)。然而,插槽内渲染的内容将取决于导航类型:

  • 软导航:在客户端导航期间,Next.js 将执行部分渲染,更改插槽内的子页面,同时保持其他插槽的活动子页面,即使它们与当前 URL 不匹配。
  • 硬导航:在完整页面加载(浏览器刷新)之后,Next.js 无法确定与当前 URL 不匹配的插槽的活动状态。相反,它将为不匹配的插槽渲染 default.js 文件,如果 default.js 不存在则渲染 404

值得注意的是

  • 不匹配路由的 404 有助于确保你不会意外地在不适合的页面上渲染 parallel route。

示例

使用 useSelectedLayoutSegment(s)

useSelectedLayoutSegmentuseSelectedLayoutSegments 都接受一个 parallelRoutesKey 参数,允许你读取插槽内的活动路由段。

app/layout.tsx
TypeScript
'use client'
 
import { useSelectedLayoutSegment } from 'next/navigation'
 
export default function Layout({ auth }: { auth: React.ReactNode }) {
  const loginSegment = useSelectedLayoutSegment('auth')
  // ...
}

当用户导航到 app/@auth/login(或在 URL 栏中输入 /login)时,loginSegment 将等于字符串 "login"

条件路由

你可以使用 Parallel Routes 根据特定条件有条件地渲染路由,例如用户角色。例如,为 /admin/user 角色渲染不同的仪表盘页面:

Conditional routes diagram
app/dashboard/layout.tsx
TypeScript
import { checkUserRole } from '@/lib/auth'
 
export default function Layout({
  user,
  admin,
}: {
  user: React.ReactNode
  admin: React.ReactNode
}) {
  const role = checkUserRole()
  return role === 'admin' ? admin : user
}

标签组

你可以在插槽内添加 layout,以允许用户独立导航插槽。这对于创建标签页非常有用。

例如,@analytics 插槽有两个子页面:/page-views/visitors

Analytics slot with two subpages and a layout

@analytics 内,创建一个 layout 文件,在两个页面之间共享标签页:

app/@analytics/layout.tsx
TypeScript
import Link from 'next/link'
 
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Link href="/page-views">Page Views</Link>
        <Link href="/visitors">Visitors</Link>
      </nav>
      <div>{children}</div>
    </>
  )
}

模态框

Parallel Routes 可以与 Intercepting Routes 一起使用,创建支持深层链接的模态框。这使你能够解决构建模态框时的常见挑战,例如:

  • 使模态框内容可通过 URL 共享
  • 在页面刷新时保留上下文,而不是关闭模态框。
  • 在后退导航时关闭模态框,而不是前往上一个路由。
  • 在前进导航时重新打开模态框

考虑以下 UI 模式,用户可以使用客户端导航从布局中打开登录模态框,或访问单独的 /login 页面:

Parallel Routes Diagram

要实现这种模式,首先创建一个 /login 路由,渲染你的登录页面。

Parallel Routes Diagram
app/login/page.tsx
TypeScript
import { Login } from '@/app/ui/login'
 
export default function Page() {
  return <Login />
}

然后,在 @auth 插槽内,添加返回 nulldefault.js 文件。这确保了模态框在不活动时不会被渲染。

app/@auth/default.tsx
TypeScript
export default function Default() {
  return null
}

在你的 @auth 插槽内,通过将 <Modal> 组件及其子组件导入到 @auth/(.)login/page.tsx 文件中来拦截 /login 路由,并将文件夹名称更新为 /@auth/(.)login/page.tsx

app/@auth/(.)login/page.tsx
TypeScript
import { Modal } from '@/app/ui/modal'
import { Login } from '@/app/ui/login'
 
export default function Page() {
  return (
    <Modal>
      <Login />
    </Modal>
  )
}

值得注意的是

打开模态框

现在,你可以利用 Next.js router 来打开和关闭模态框。这确保了在模态框打开时以及在前进和后退导航时 URL 都能正确更新。

要打开模态框,将 @auth 插槽作为 prop 传递给父布局,并将其与 children prop 一起渲染。

app/layout.tsx
TypeScript
import Link from 'next/link'
 
export default function Layout({
  auth,
  children,
}: {
  auth: React.ReactNode
  children: React.ReactNode
}) {
  return (
    <>
      <nav>
        <Link href="/login">Open modal</Link>
      </nav>
      <div>{auth}</div>
      <div>{children}</div>
    </>
  )
}

当用户点击 <Link> 时,模态框将打开而不是导航到 /login 页面。然而,在刷新或初始加载时,导航到 /login 将把用户带到主登录页面。

关闭模态框

你可以通过调用 router.back() 或使用 Link 组件来关闭模态框。

app/ui/modal.tsx
TypeScript
'use client'
 
import { useRouter } from 'next/navigation'
 
export function Modal({ children }: { children: React.ReactNode }) {
  const router = useRouter()
 
  return (
    <>
      <button
        onClick={() => {
          router.back()
        }}
      >
        Close modal
      </button>
      <div>{children}</div>
    </>
  )
}

当使用 Link 组件导航离开不应再渲染 @auth 插槽的页面时,我们需要确保 parallel route 匹配到一个返回 null 的组件。例如,当导航回根页面时,我们创建一个 @auth/page.tsx 组件:

app/ui/modal.tsx
TypeScript
import Link from 'next/link'
 
export function Modal({ children }: { children: React.ReactNode }) {
  return (
    <>
      <Link href="/">Close modal</Link>
      <div>{children}</div>
    </>
  )
}
app/@auth/page.tsx
TypeScript
export default function Page() {
  return null
}

或者,如果导航到任何其他页面(例如 /foo/foo/bar 等),你可以使用通配插槽:

app/@auth/[...catchAll]/page.tsx
TypeScript
export default function CatchAll() {
  return null
}

值得注意的是

  • 我们在 @auth 插槽中使用通配路由来关闭模态框,这是因为 parallel routes 的行为方式。由于客户端导航到不再匹配插槽的路由时将保持可见,我们需要将插槽匹配到一个返回 null 的路由来关闭模态框。
  • 其他示例可能包括在画廊中打开照片模态框的同时拥有专用的 /photo/[id] 页面,或在侧边模态框中打开购物车。
  • 查看示例:使用 Intercepted 和 Parallel Routes 的模态框。

Loading 和 Error UI

Parallel Routes 可以独立流式传输,允许你为每个路由定义独立的错误和加载状态:

Parallel routes enable custom error and loading states

有关更多信息,请参阅 Loading UIError Handling 文档。