Skip to main content

平行路由

平行路由允许您在同一布局中同时或有条件地渲染一个或多个页面。它们对于应用的高度动态部分非常有用,例如社交网站的网站主页和源。

例如,考虑到仪表板,您可以使用平行路由来同时渲染团队分析页面:

Parallel Routes Diagram

插槽

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

Parallel Routes File-system Structure

插槽作为属性传递到共享的父布局。对于上面的示例,app/layout.js中的组件目前接收@analytics@team插槽属性,并能够将它们与children属性进行平行渲染:

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是插槽。

您需要知道:

  • children 属性是不需要映射到文件夹的内置插槽。这意味着app/page.js等同于app/@children/page.js

活跃状态和导航

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

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

您需要知道:

  • 对于不匹配的路由,404有助于确保您不会意外地在页面上渲染不打算渲染的平行路由。

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 无法恢复父页面的活动状态时,为children创建一个渲染备用的default.js文件。

useSelectedLayoutSegment(s)

useSelectedLayoutSegmentuseSelectedLayoutSegments 都接收一个 parallelRoutesKey 参数, 它允许您读取插槽中的活动路由段。

"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 (或路由栏的/login)时,loginSegment将相当于字符串"login"

例子

条件路由

您能够使用平行路由根据某些条件来按条件渲染路由(比如用户角色)。例如,为/admin/user 角色渲染不同的网站主页。

Conditional routes diagram
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,以允许用户用户独立导航插槽。这对于创建 tabs 非常有用。

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

Analytics slot with two subpages and a layout

@analytics中, 创建一个layout 文件以在两个页面之间共享 tabs。

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 }: { children: React.ReactNode }) {
return (
<>
<nav>
<Link href="/page-views">Page Views</Link>
<Link href="/visitors">Visitors</Link>
</nav>
<div>{children}</div>
</>
);
}

Modals

平行路由可能与拦截路由一起使用来创建支持深度链接的 modals。这允许您解决构建时的常见问题, 例如:

  • 使模态内容可通过 URL 共享
  • 在页面刷新时保留上下文, 而不是关闭 modal。
  • 回退导航时关闭 modal,而不是返回到上一个路由。
  • 继续导航时重新打开 modal.

请考虑以下的 UI 模式, 这里用户可以使用客户端导航打开 layout 的登录 modal, 或者访问单独的/login页面:

Parallel Routes Diagram

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

Parallel Routes Diagram
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插槽内, 添加 returns nulldefault.js文件。这会确保 modal 在不活跃的状态下不会渲染。

export default function Default() {
return "...";
}
export default function Default() {
return "...";
}

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

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>
);
}

您需要知道:

  • 用于拦截路由的约定, 例如(.), 取决于您的文件系统结构。参阅拦截路由约定
  • 通过将<Modal>功能与 modal 内容(<Login>)分离, 您能够确保 modal 内的任何内容, 如forms, 都是服务器组件。参阅拦截客户端和服务器组件来获取更多信息。

打开 modal

现在, 您能够使用 Next.js 路由来开关 modal。这确保在 modal 打开时或导航前进或后退时,正确地更新 URL。

为了打开 modal,将@auth 插槽作为属性传递给父布局,将其与 children属性一起渲染。

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>时, modal 将会打开而不是导航到/login页面。不过,在刷新或初始加载时, 导航会将用户带到主登录页/login

关闭 modal

您能够通过调用router.back() 或通过使用Link组件来关闭 modal。

"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插槽的页面时,我们需要确保平行路由匹配 returns null的组件。例如, 导航回到根页面时, 创建一个@auth/page.tsx组件:

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 "...";
}
export default function Page() {
return "...";
}

或者如果导航到其它页面时(例如 /foo, /foo/bar, 等), 你能够使用 catch-all 插槽:

export default function CatchAll() {
return "...";
}
export default function CatchAll() {
return "...";
}

您需要知道:

  • 由于活跃状态和导航中描述的行为,我们在@auth插槽中使用 catch-all 路由来关闭 modal。因为客户端导航到不再匹配插槽的路由仍然是可见的,我们需要将插槽匹配到返回' null '的路由来关闭 modal。
  • 查看带有拦截和平行路由的 modals示例

Loading 和 Error UI

平行路由可以独立流式传输, 允许您为每个路由定义 error and loading 状态:

Parallel routes enable custom error and loading states

参阅Loading UI错误处理 文档获取更多信息。