并行路由 (Parallel Routes) 允许您在同一个布局中同时或有条件地渲染一个或多个页面。它们对于应用中高度动态的部分非常有用,例如社交网站上的仪表盘和动态信息流。
例如,考虑一个仪表盘,您可以使用并行路由同时渲染 team 和 analytics 页面:

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

插槽作为 props 传递给共享的父级布局。对于上面的示例,app/layout.js 中的组件现在接受 @analytics 和 @team 插槽 prop,并可以与 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 是一个插槽。插槽与常规的 Page 组件结合,形成与路由段关联的最终页面。因此,您不能在同一路由段级别拥有单独的 静态 和 动态 插槽。如果一个插槽是动态的,那么该级别的所有插槽都必须是动态的。
须知:
childrenprop 是一个隐式插槽,无需映射到文件夹。这意味着app/page.js等同于app/@children/page.js。
default.js您可以定义一个 default.js 文件,在初始加载或整页重新加载期间,作为不匹配插槽的后备渲染。
考虑以下文件夹结构。@team 插槽有一个 /settings 页面,但 @analytics 没有。

当导航到 /settings 时,@team 插槽将渲染 /settings 页面,同时保持 @analytics 插槽当前激活的页面。
刷新时,Next.js 将为 @analytics 渲染一个 default.js。如果 default.js 不存在,则会渲染 404。
此外,由于 children 是一个隐式插槽,您还需要创建一个 default.js 文件,以便在 Next.js 无法恢复父页面活动状态时,为 children 渲染后备内容。
默认情况下,Next.js 会跟踪每个插槽的活动状态(或子页面)。然而,插槽中渲染的内容将取决于导航类型:
default.js 文件,如果 default.js 不存在,则渲染 404。须知:
- 不匹配路由的
404有助于确保您不会在不适合的页面上意外渲染并行路由。
useSelectedLayoutSegment(s)useSelectedLayoutSegment 和 useSelectedLayoutSegments 都接受一个 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(或 URL 栏中的 /login)时,loginSegment 将等于字符串 "login"。
您可以使用并行路由根据特定条件(例如用户角色)有条件地渲染路由。例如,为 /admin 或 /user 角色渲染不同的仪表盘页面:

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;
}您可以在插槽内添加一个 layout,以允许用户独立导航该插槽。这对于创建选项卡很有用。
例如,@analytics 插槽有两个子页面:/page-views 和 /visitors。

在 @analytics 内部,创建一个 layout 文件,以在两个页面之间共享选项卡:
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>
</>
);
}并行路由可以与 拦截路由 结合使用,创建支持深层链接的模态框。这使您能够解决构建模态框时常见的挑战,例如:
考虑以下 UI 模式,用户可以通过客户端导航从布局中打开登录模态框,或者访问单独的 /login 页面:

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

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 插槽内部,添加一个返回 null 的 default.js 文件。这确保了模态框在不活动时不被渲染。
export default function Default() {
return null;
}export default function Default() {
return null;
}在您的 @auth 插槽内部,通过将 <Modal> 组件及其子组件导入到 @auth/(.)login/page.tsx 文件来拦截 /login 路由,并将文件夹名称更新为 /@auth/(.)login/page.tsx。
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>功能与模态框内容(<Login>)分离,您可以确保模态框内的任何内容(例如表单)都是服务器组件。有关更多信息,请参阅 客户端组件与服务器组件的交错使用。
现在,您可以使用 Next.js 路由器来打开和关闭模态框。这确保了在模态框打开时以及向后和向前导航时,URL 能够正确更新。
要打开模态框,请将 @auth 插槽作为 prop 传递给父级布局,并与 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 将把用户带到主登录页面。
您可以通过调用 router.back() 或使用 Link 组件来关闭模态框。
"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 组件:
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 等),您可以使用一个全捕获插槽:
export default function CatchAll() {
return null;
}export default function CatchAll() {
return null;
}须知:
- 我们在
@auth插槽中使用全捕获路由来关闭模态框,这是因为并行路由的行为方式。由于客户端导航到一个不再匹配插槽的路由时,该插槽会保持可见,因此我们需要将插槽匹配到一个返回null的路由来关闭模态框。- 其他示例可能包括在图库中打开照片模态框,同时也有一个专用的
/photo/[id]页面,或者在侧边模态框中打开购物车。- 查看一个 结合了拦截路由和并行路由的模态框示例。
并行路由可以独立流式传输,允许您为每个路由定义独立的错误和加载状态:
