Skip to main content

并行路由

并行路由允许你同时或有条件地渲染同一布局中的一个或多个页面。它们对于应用的高度动态部分非常有用,例如仪表板和社交网站上的提要。

¥Parallel Routes allows you to simultaneously or conditionally render one or more pages within the same layout. They are useful for highly dynamic sections of an app, such as dashboards and feeds on social sites.

例如,考虑仪表板,你可以使用并行路由同时渲染 teamanalytics 页面:

¥For example, considering a dashboard, you can use parallel routes to simultaneously render the team and analytics pages:

插槽

¥Slots

并行路由是使用命名槽创建的。插槽是按照 @folder 约定定义的。例如,以下文件结构定义了两个槽:@analytics@team

¥Parallel routes are created using named slots. Slots are defined with the @folder convention. For example, the following file structure defines two slots: @analytics and @team:

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

¥Slots are passed as props to the shared parent layout. For the example above, the component in app/layout.js now accepts the @analytics and @team slots props, and can render them in parallel alongside the children prop:

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

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

¥However, slots are not route segments and do not affect the URL structure. For example, for /@analytics/views, the URL will be /views since @analytics is a slot. Slots are combined with the regular Page component to form the final page associated with the route segment. Because of this, you cannot have separate static and dynamic slots at the same route segment level. If one slot is dynamic, all slots at that level must be dynamic.

很高兴知道:

¥Good to know:

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

    ¥The children prop is an implicit slot that does not need to be mapped to a folder. This means app/page.js is equivalent to app/@children/page.js.

活动状态和导航

¥Active state and navigation

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

¥By default, Next.js keeps track of the active state (or subpage) for each slot. However, the content rendered within a slot will depend on the type of navigation:

  • 软导航:在客户端导航期间,Next.js 将执行 部分渲染,更改插槽内的子页面,同时保留其他插槽的活动子页面,即使它们与当前 URL 不匹配。

    ¥Soft Navigation: During client-side navigation, Next.js will perform a partial render, changing the subpage within the slot, while maintaining the other slot's active subpages, even if they don't match the current URL.

  • 硬导航:全页加载(浏览器刷新)后,Next.js 无法确定与当前 URL 不匹配的插槽的活动状态。相反,它将为不匹配的插槽渲染 default.js 文件,如果 default.js 不存在,则渲染 404

    ¥Hard Navigation: After a full-page load (browser refresh), Next.js cannot determine the active state for the slots that don't match the current URL. Instead, it will render a default.js file for the unmatched slots, or 404 if default.js doesn't exist.

很高兴知道:

¥Good to know:

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

    ¥The 404 for unmatched routes helps ensure that you don't accidentally render a parallel route on a page that it was not intended for.

default.js

你可以定义一个 default.js 文件来渲染,作为初始加载或整页重新加载期间不匹配插槽的后备。

¥You can define a default.js file to render as a fallback for unmatched slots during the initial load or full-page reload.

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

¥Consider the following folder structure. The @team slot has a /settings page, but @analytics does not.

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

¥When navigating to /settings, the @team slot will render the /settings page while maintaining the currently active page for the @analytics slot.

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

¥On refresh, Next.js will render a default.js for @analytics. If default.js doesn't exist, a 404 is rendered instead.

此外,由于 children 是隐式插槽,因此你还需要创建 default.js 文件,以便在 Next.js 无法恢复父页面的活动状态时渲染 children 的后备。

¥Additionally, since children is an implicit slot, you also need to create a default.js file to render a fallback for children when Next.js cannot recover the active state of the parent page.

useSelectedLayoutSegment(s)

useSelectedLayoutSegmentuseSelectedLayoutSegments 都接受 parallelRoutesKey 参数,该参数允许你读取槽内的活动航路段。

¥Both useSelectedLayoutSegment and useSelectedLayoutSegments accept a parallelRoutesKey parameter, which allows you to read the active route segment within a slot.

'use client'

import { useSelectedLayoutSegment } from 'next/navigation'

export default function Layout({ auth }: { auth: React.ReactNode }) {
const loginSegment = useSelectedLayoutSegment('auth')
// ...
}
'use client'

import { useSelectedLayoutSegment } from 'next/navigation'

export default function Layout({ auth }) {
const loginSegment = useSelectedLayoutSegment('auth')
// ...
}

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

¥When a user navigates to app/@auth/login (or /login in the URL bar), loginSegment will be equal to the string "login".

示例

¥Examples

条件路由

¥Conditional Routes

你可以使用并行路由根据某些条件(例如用户角色)有条件地渲染路由。例如,要为 /admin/user 角色渲染不同的仪表板页面:

¥You can use Parallel Routes to conditionally render routes based on certain conditions, such as user role. For example, to render a different dashboard page for the /admin or /user roles:

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
}
import { checkUserRole } from '@/lib/auth'

export default function Layout({ user, admin }) {
const role = checkUserRole()
return role === 'admin' ? admin : user
}

选项卡组

¥Tab Groups

你可以在插槽内添加 layout,以允许用户独立导航该插槽。这对于创建选项卡很有用。

¥You can add a layout inside a slot to allow users to navigate the slot independently. This is useful for creating tabs.

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

¥For example, the @analytics slot has two subpages: /page-views and /visitors.

@analytics 中,创建一个 layout 文件以共享两个页面之间的选项卡:

¥Within @analytics, create a layout file to share the tabs between the two pages:

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>
</>
)
}
import Link from 'next/link'

export default function Layout({ children }) {
return (
<>
<nav>
<Link href="/page-views">Page Views</Link>
<Link href="/visitors">Visitors</Link>
</nav>
<div>{children}</div>
</>
)
}

模态

¥Modals

并行路由可以与 拦截路由 一起使用,以创建支持深度链接的模式。这使你可以解决构建模式时的常见挑战,例如:

¥Parallel Routes can be used together with Intercepting Routes to create modals that support deep linking. This allows you to solve common challenges when building modals, such as:

  • 使模态内容可通过 URL 共享。

    ¥Making the modal content shareable through a URL.

  • 刷新页面时保留上下文,而不是关闭模式。

    ¥Preserving context when the page is refreshed, instead of closing the modal.

  • 关闭向后导航的模式,而不是转到上一个路由。

    ¥Closing the modal on backwards navigation rather than going to the previous route.

  • 重新打开向前导航的模式。

    ¥Reopening the modal on forwards navigation.

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

¥Consider the following UI pattern, where a user can open a login modal from a layout using client-side navigation, or access a separate /login page:

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

¥To implement this pattern, start by creating a /login route that renders your main login page.

import { Login } from '@/app/ui/login'

export default function Page() {
return <Login />
}
import { Login } from '@/app/ui/login'

export default function Page() {
return <Login />
}

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

¥Then, inside the @auth slot, add default.js file that returns null. This ensures that the modal is not rendered when it's not active.

export default function Default() {
return null
}
export default function Default() {
return null
}

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

¥Inside your @auth slot, intercept the /login route by updating the /(.)login folder. Import the <Modal> component and its children into the /(.)login/page.tsx file:

import { Modal } from '@/app/ui/modal'
import { Login } from '@/app/ui/login'

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

export default function Page() {
return (
<Modal>
<Login />
</Modal>
)
}

很高兴知道:

¥Good to know:

  • 用于拦截路由的约定,例如 (.),取决于你的文件系统结构。参见 拦截路由约定

    ¥The convention used to intercept the route, e.g. (.), depends on your file-system structure. See Intercepting Routes convention.

  • 通过将 <Modal> 功能与模态内容 (<Login>) 分离,你可以确保模态内的任何内容,例如 forms,是服务器组件。请参阅 交错客户端和服务器组件 了解更多信息。

    ¥By separating the <Modal> functionality from the modal content (<Login>), you can ensure any content inside the modal, e.g. forms, are Server Components. See Interleaving Client and Server Components for more information.

打开模式

¥Opening the modal

现在,你可以利用 Next.js 路由来打开和关闭模式。这可确保在模式打开以及前后导航时正确更新 URL。

¥Now, you can leverage the Next.js router to open and close the modal. This ensures the URL is correctly updated when the modal is open, and when navigating backwards and forwards.

要打开模式,请将 @auth 插槽作为属性传递给父布局,并将其与 children 属性一起渲染。

¥To open the modal, pass the @auth slot as a prop to the parent layout and render it alongside the children prop.

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>
</>
)
}
import Link from 'next/link'

export default function Layout({ auth, children }) {
return (
<>
<nav>
<Link href="/login">Open modal</Link>
</nav>
<div>{auth}</div>
<div>{children}</div>
</>
)
}

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

¥When the user clicks the <Link>, the modal will open instead of navigating to the /login page. However, on refresh or initial load, navigating to /login will take the user to the main login page.

关闭模式

¥Closing the modal

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

¥You can close the modal by calling router.back() or by using the Link component.

'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>
</>
)
}
'use client'

import { useRouter } from 'next/navigation'

export function Modal({ children }) {
const router = useRouter()

return (
<>
<button
onClick={() => {
router.back()
}}
>
Close modal
</button>
<div>{children}</div>
</>
)
}

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

¥When using the Link component to navigate away from a page that shouldn't render the @auth slot anymore, we need to make sure the parallel route matches to a component that returns null. For example, when navigating back to the root page, we create a @auth/page.tsx component:

import Link from 'next/link'

export function Modal({ children }: { children: React.ReactNode }) {
return (
<>
<Link href="/">Close modal</Link>
<div>{children}</div>
</>
)
}
import Link from 'next/link'

export function Modal({ children }) {
return (
<>
<Link href="/">Close modal</Link>
<div>{children}</div>
</>
)
}
export default function Page() {
return null
}
export default function Page() {
return null
}

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

¥Or if navigating to any other page (such as /foo, /foo/bar, etc), you can use a catch-all slot:

export default function CatchAll() {
return null
}
export default function CatchAll() {
return null
}

很高兴知道:

¥Good to know:

  • 由于 活动状态和导航 中描述的行为,我们在 @auth 槽中使用包罗万象的路由来关闭模式。由于客户端导航到不再与插槽匹配的路由将保持可见,因此我们需要将插槽与返回 null 的路由相匹配以关闭模式。

    ¥We use a catch-all route in our @auth slot to close the modal because of the behavior described in Active state and navigation. Since client-side navigations to a route that no longer match the slot will remain visible, we need to match the slot to a route that returns null to close the modal.

  • 其他示例可能包括在图库中打开照片模式,同时还有专用的 /photo/[id] 页面,或者在侧面模式中打开购物车。

    ¥Other examples could include opening a photo modal in a gallery while also having a dedicated /photo/[id] page, or opening a shopping cart in a side modal.

  • 查看示例 具有拦截和并行路由的模态。

    ¥View an example of modals with Intercepted and Parallel Routes.

加载和错误 UI

¥Loading and Error UI

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

¥Parallel Routes can be streamed independently, allowing you to define independent error and loading states for each route:

有关详细信息,请参阅 加载用户界面错误处理 文档。

¥See the Loading UI and Error Handling documentation for more information.