并行路由
并行路由允许你在同一布局中同时或有条件地渲染一个或多个页面。它们对于应用中高度动态的部分非常有用,例如仪表盘和社交网站上的信息流。
例如,考虑一个仪表盘,你可以使用并行路由同时渲染"team"和"analytics"页面:
插槽
并行路由通过命名插槽创建。插槽使用@folder
约定定义。例如,以下文件结构定义了两个插槽:@analytics
和@team
:
插槽作为props传递给共享的父级布局。对于上面的例子,现在app/layout.js
中的组件接受@analytics
和@team
插槽props,并可以与children
prop一起并行渲染它们:
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
。
活动状态和导航
默认情况下,Next.js会跟踪每个插槽的活动_状态_(或子页面)。然而,插槽内渲染的内容将取决于导航类型:
- 软导航:在客户端导航期间,Next.js将执行部分渲染,改变插槽内的子页面,同时保持其他插槽的活动子页面,即使它们与当前URL不匹配。
- 硬导航:在完整页面加载(浏览器刷新)后,Next.js无法确定与当前URL不匹配的插槽的活动状态。相反,它将为不匹配的插槽渲染一个
default.js
文件,如果default.js
不存在,则渲染404
。
值得注意的是:
- 对于不匹配路由的
404
有助于确保你不会意外地在未打算的页面上渲染并行路由。
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
渲染一个回退。
useSelectedLayoutSegment(s)
useSelectedLayoutSegment
和useSelectedLayoutSegments
都接受一个parallelRoutesKey
参数,它允许你读取插槽内的活动路由段。
'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"
。
示例
条件路由
你可以使用并行路由基于某些条件有条件地渲染路由,例如用户角色。例如,为/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
}
标签组
你可以在插槽内添加一个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>
</>
)
}
模态框
并行路由可以与拦截路由一起使用,创建支持深度链接的模态框。这允许你解决构建模态框时常见的挑战,例如:
- 使模态框内容可以通过URL共享。
- 页面刷新时保留上下文,而不是关闭模态框。
- 后退导航时关闭模态框,而不是返回到上一个路由。
- 前进导航时重新打开模态框。
考虑以下UI模式,用户可以使用客户端导航从布局打开登录模态框,或访问单独的/login
页面:
要实现这种模式,首先创建一个/login
路由,渲染你的主要登录页面。
import { Login } from '@/app/ui/login'
export default function Page() {
return <Login />
}
然后,在@auth
插槽内,添加一个返回null
的default.js
文件。这确保在模态框不活动时不会渲染它。
export default function Default() {
return null
}
在你的@auth
插槽内,通过更新/(.)login
文件夹来拦截/login
路由。将<Modal>
组件及其子组件导入到/(.)login/page.tsx
文件中:
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>
</>
)
}
当用户点击<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>
</>
)
}
当使用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>
</>
)
}
export default function Page() {
return null
}
或者如果导航到任何其他页面(例如/foo
、/foo/bar
等),你可以使用一个捕获所有插槽:
export default function CatchAll() {
return null
}
值得注意的是:
加载和错误UI
并行路由可以独立流式传输,允许你为每个路由定义独立的错误和加载状态: