Next.js 并行路由和拦截路由解决什么问题?
并行路由用 @folder 定义插槽,让同一个布局同时渲染几块可以独立导航的页面,插槽不影响 URL,刷新后匹配不上的插槽渲染 default.js,没有就 404。拦截路由用 (.)、(..)、(...) 在当前布局里加载另一个路由,常和并行路由一起做可分享 URL 的模态框:站内点击弹出模态框,刷新或直接访问显示完整页面。
简答
比如图片流页面,点一张图希望弹出模态框,地址栏变成
/photo/123方便分享;别人直接打开这个链接,看到的是完整的图片详情页。Next.js App Router 用并行路由加拦截路由来做这件事。并行路由是在一个布局里同时放几块“插槽”,文件夹用
@开头,比如@team、@analytics。插槽会作为 props 传给父布局,每块能独立导航,也能有自己的 loading 和错误界面。插槽不是路由段,不会出现在 URL 里。拦截路由是在当前布局里加载另一个路由的内容。文件夹名用
(.)、(..)、(..)(..)、(...)表示同级、上一级、上两级和根目录,按路由段算,不按文件夹层级算。只有站内的客户端导航会被拦截;刷新或者直接访问,渲染的是原本的完整页面。最容易出问题的是刷新。站内跳转时,匹配不上当前 URL 的插槽会保留原来的内容;刷新后 Next.js 无法恢复这些状态,会渲染插槽里的
default.js,没有就直接 404。所以每个插槽,包括隐式的children,通常都要准备default.js。
并行路由怎样写
假设仪表盘要同时显示团队和统计两块。目录里建 app/@team 和 app/@analytics,父布局就会多出两个同名的 props:
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<>
{children}
{team}
{analytics}
</>
);
}
children 也是一个插槽,只是不用建文件夹,app/page.js 就等于 app/@children/page.js。插槽不影响地址,app/@analytics/views 对应的 URL 是 /views。
插槽可以按条件渲染,比如管理员返回 admin 插槽,普通用户返回 user 插槽。官方文档特别提醒:两个插槽都会在服务器上渲染,布局里的条件只决定用户看到哪一块,@admin/page.js 里的数据请求对每个用户都会执行。所以每个插槽的页面里要自己做权限校验。
还有一个限制:插槽和普通页面合起来组成这个路由段的最终页面,所以同一层级的插槽不能有的静态预渲染、有的动态渲染,只要有一个是动态的,同级的都得是动态的。
刷新以后为什么 404
插槽在两种导航下表现不同,这是并行路由最容易踩的坑。
站内用 <Link> 跳转是软导航,Next.js 会记住每个插槽当前显示的子页面。跳到 /settings 时,如果 @team 有 settings 页面而 @analytics 没有,@team 换成新页面,@analytics 继续显示原来的内容。
刷新页面是硬导航,Next.js 不知道 @analytics 之前显示什么,于是渲染 @analytics/default.js;如果没有这个文件,整页 404。官方的解释是,这个 404 能避免把并行路由渲染到原本不打算出现的页面上。children 也一样,恢复不了父页面状态时同样需要 default.js。
用拦截路由做可分享的模态框
拦截路由的约定类似相对路径 ../,只是按路由段计算。@modal 这种插槽文件夹不算路由段,所以 app/@modal/(.)photo/[id] 里的 (.) 指的是和插槽所在位置同级的 photo 路由。
做模态框需要几个文件配合。app/photo/[id]/page.tsx 是完整的详情页;app/@modal/default.tsx 返回 null,保证平时不显示模态框;app/@modal/(.)photo/[id]/page.tsx 把详情内容包在模态框组件里;根布局同时渲染 {modal} 和 {children}。
这样站内点击图片时,/photo/123 被拦截,模态框盖在图片流上;刷新或者直接打开链接,不会发生拦截,显示完整详情页。官方列出的好处是:模态框内容可以通过 URL 分享,刷新后不丢上下文,后退关闭模态框,前进重新打开。
关闭模态框可以调用 router.back()。如果用 <Link> 跳到别的页面,要注意软导航下插槽会保留旧内容,模态框不会自己消失,所以要给插槽准备一个返回 null 的页面,比如 @modal/page.tsx 或者兜底的 @modal/[...catchAll]/page.tsx。
模态框本身的焦点和键盘行为,见《一个可访问的模态对话框需要具备哪些行为?》。
常见追问
为什么 (..) 有时候和文件夹层级对不上?
因为它按路由段算,不按文件系统算。
@slot文件夹不是路由段,官方例子里photo在文件系统上高两级,但按路由段只高一级,所以写(..)就够了。
并行路由除了模态框还能做什么?
官方给出的例子有仪表盘同时显示多块内容、按用户角色渲染不同插槽、在插槽里放布局做独立切换的标签页,以及给每块内容单独设置 loading 和错误界面,让它们各自流式加载。
插槽里的组件能读到当前激活的子路由吗?
可以用
useSelectedLayoutSegment或useSelectedLayoutSegments,传入插槽名作为parallelRouteKey。比如访问/login时,useSelectedLayoutSegment('auth')返回"login"。