前端知库

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:

TSX
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"。