布局系统的设计哲学
在 App Router 之前,Next.js 的布局需要开发者手动组装——高阶组件、自定义 _app.js、条件渲染...不同项目的实现五花八门。
App Router 的布局系统基于一个简单的洞察:大多数应用的页面结构是层级化的,不同层级有不同的共享元素。
想象一个后台管理系统:
- 顶层有导航栏(所有页面共享)
- 左侧有菜单栏(管理页面共享)
- 右侧内容区(每个页面不同)
这种层级结构用文件夹自然表达:
app/
├── layout.tsx # 根布局:导航栏
├── dashboard/
│ ├── layout.tsx # 仪表盘布局:侧边栏
│ ├── page.tsx # 仪表盘首页
│ └── settings/
│ └── page.tsx # 设置页
└── auth/
├── layout.tsx # 认证布局:居中卡片
└── login/
└── page.tsx # 登录页
每个 layout.tsx 自动包裹其下的所有页面,无需手动配置。
布局与模板的区别
App Router 提供了两个看起来相似的概念:layout.tsx 和 template.tsx。它们的关键区别在于状态保持。
Layout(布局):
- 导航时保持挂载,状态不丢失
- 适合全局导航、侧边栏、播放器等需要持久化的 UI
- 只在首次渲染时执行
Template(模板):
- 每次导航都会重新创建实例
- 适合需要重置状态的场景(过渡动画、表单重置)
- 每次路由变化都执行
实际使用中,90% 的场景用 Layout 就够了。Template 主要用于特定的动效需求。
何时使用 Template
几个典型场景:
入场动画:希望每次进入页面都播放动画,而不是只有首次进入。
表单重置:切换不同的编辑页面时,希望表单状态清空。
第三方库状态:某些库需要在"进入"页面时初始化,Layout 只会初始化一次。
使用方式和 Layout 完全一样,只是文件名不同:
app/
├── template.tsx # 每次导航都重新渲染
├── layout.tsx # 保持状态
└── page.tsx
如果同时存在,渲染顺序是:Layout > Template > Page。
嵌套布局的最佳实践
状态提升与下沉
布局嵌套后,一个常见问题是:某个状态应该放在哪一层?
原则是:状态放在使用它的最近公共祖先。
如果一个状态只在某个页面用,放在页面组件里。如果需要跨多个页面共享,提升到包含这些页面的布局中。
不要把所有状态都堆到根布局——那会导致不必要的重渲染和状态混乱。
布局间通信
布局之间如何传递信息?几种方式:
URL 参数:最简单直接,适合影响路由的状态(搜索条件、筛选器)。
Context:适合 UI 状态(主题、侧边栏展开状态)。需要在布局中提供 Provider。
全局状态库:复杂应用用 Zustand/Jotai 等。但要注意这些库是客户端的,布局如果是 Server Component 就不能直接用。
避免布局陷阱
陷阱一:在布局中获取页面级数据
布局是共享的,不应该获取特定页面的数据。每个页面的数据获取应该在 page.tsx 中完成。
陷阱二:布局过于臃肿
把太多逻辑塞进布局会拖慢所有子页面。布局应该只包含真正需要共享的 UI 和最小必要的逻辑。
陷阱三:忽视水合成本
即使布局是 Server Component,如果包含的 Client Component 过多,水合成本仍然很高。审视布局中的 'use client' 组件是否必要。
加载状态设计
loading.tsx 的工作原理
在任何路由段放置 loading.tsx,Next.js 会自动用它包裹 page.tsx,创建一个 Suspense 边界。
app/
├── dashboard/
│ ├── loading.tsx # 加载中显示
│ └── page.tsx # 实际内容
当用户导航到 /dashboard 时:
- 立即显示
loading.tsx的内容 page.tsx在后台加载- 加载完成后,平滑替换
分层加载状态
复杂页面可能需要多个加载状态。利用嵌套结构实现分层加载:
app/
├── dashboard/
│ ├── loading.tsx # 整体骨架
│ ├── page.tsx
│ └── analytics/
│ ├── loading.tsx # 分析部分骨架
│ └── page.tsx
用户从首页导航到分析页时:
- 首先显示 dashboard 的 loading
- Dashboard 加载完成,显示 analytics 的 loading
- Analytics 加载完成,显示最终内容
骨架屏设计建议
好的骨架屏不只是"灰色方块":
匹配实际布局:骨架屏的结构应该和真实内容一致,减少布局跳动。
渐进式细节:先显示大块结构,再显示小元素。不需要每个文字都有对应的骨架线。
品牌一致性:骨架屏也是品牌体验的一部分,颜色、动效应该和整体设计语言一致。
避免过度动画:闪烁动画看多了会视觉疲劳,微妙的渐变更舒适。
并行路由进阶
什么是并行路由
并行路由允许在同一个布局中同时渲染多个页面,各自独立加载和导航。
典型场景:
- 主内容 + 侧边详情面板
- 仪表盘多个独立的数据卡片
- 列表 + 详情的主从视图
插槽语法
并行路由通过 @ 前缀的文件夹定义:
app/
├── layout.tsx
├── page.tsx
├── @modal/
│ └── login/
│ └── page.tsx
└── @sidebar/
└── default.tsx
在 layout.tsx 中,这些插槽作为 props 传入:
export default function Layout({
children,
modal,
sidebar
}: {
children: React.ReactNode
modal: React.ReactNode
sidebar: React.ReactNode
}) {
return (
<div>
<main>{children}</main>
<aside>{sidebar}</aside>
{modal}
</div>
)
}
default.tsx 的作用
当某个插槽没有匹配的内容时,显示 default.tsx。这是并行路由正常工作的关键。
没有 default.tsx 时,未匹配的插槽会渲染 null,可能导致布局错乱。
条件渲染
并行路由可以实现条件渲染。比如根据用户登录状态显示不同的侧边栏:
app/
├── @sidebar/
│ ├── (auth)/
│ │ └── page.tsx # 已登录显示
│ ├── (unauth)/
│ │ └── page.tsx # 未登录显示
│ └── default.tsx
通过路由组 (auth) 和 (unauth) 配合中间件,实现条件路由。
拦截路由与模态框
拦截路由的概念
拦截路由用 (.) (..) (...) 语法,可以在当前布局中"拦截"另一个路由的导航,用模态框展示而不是跳转。
典型场景:社交媒体点击图片,在当前页面弹出大图预览,但直接访问图片 URL 则显示完整的图片页面。
文件结构
app/
├── feed/
│ ├── page.tsx
│ └── @modal/
│ └── (.)photo/[id]/
│ └── page.tsx # 拦截:模态框显示
├── photo/[id]/
│ └── page.tsx # 直接访问:完整页面
. 表示拦截同级路由,.. 表示拦截上一级,... 表示拦截根目录。
模态框状态管理
拦截路由的模态框有几个特殊的状态问题:
关闭模态框:通常用 router.back() 实现。但要注意如果用户直接访问模态框 URL,back() 会离开站点。需要判断历史记录。
深链接:用户可以直接访问模态框 URL 或分享给他人。确保这种情况下体验合理。
浏览器前进后退:模态框状态要和浏览器历史同步。拦截路由天然支持这一点。
性能优化建议
布局粒度
布局粒度影响性能和复用性的平衡:
粒度太粗:根布局塞太多东西,每次导航都要处理大量不变的内容。
粒度太细:层级太深,组件树复杂,维护困难。
经验法则:一个布局对应一个"区域"的共享 UI。如果发现某些 UI 只在部分子路由共享,考虑加一层布局。
流式渲染配合
布局本身应该快速渲染,把慢的部分放在 Suspense 边界内:
export default function DashboardLayout({ children }) {
return (
<div>
<Sidebar /> {/* 快速渲染 */}
<Suspense fallback={<MainSkeleton />}>
{children} {/* 可能慢,流式加载 */}
</Suspense>
</div>
)
}
预加载策略
对于高概率访问的路由,可以提前预加载:
import Link from 'next/link'
// 鼠标悬停时预加载
<Link href="/dashboard/analytics" prefetch={true}>
分析
</Link>
// 禁用预加载(低概率访问)
<Link href="/dashboard/settings" prefetch={false}>
设置
</Link>
实战案例
案例:后台管理系统布局
典型的三栏布局结构:
app/
├── layout.tsx # 根布局:顶部导航
├── (public)/
│ ├── layout.tsx # 公开页面:简洁布局
│ └── login/
│ └── page.tsx
└── (admin)/
├── layout.tsx # 管理后台:侧边栏
├── dashboard/
│ ├── layout.tsx # 仪表盘:标签页
│ └── page.tsx
└── users/
└── page.tsx
关键点:
- 用路由组
(public)和(admin)区分不同布局 - 管理后台共享侧边栏
- 仪表盘内部有自己的标签页导航
案例:电商商品详情
主从视图 + 模态框:
app/
├── products/
│ ├── layout.tsx
│ ├── page.tsx # 商品列表
│ ├── @detail/
│ │ ├── default.tsx # 未选中时空白
│ │ └── [id]/
│ │ └── page.tsx # 侧边详情面板
│ └── @modal/
│ └── (.)quick-view/[id]/
│ └── page.tsx # 快速预览模态框
└── quick-view/[id]/
└── page.tsx # 完整预览页
用户体验:
- 列表页点击商品,右侧面板显示详情
- 点击"快速预览",弹出模态框
- 直接访问商品链接,显示完整页面
总结
Next.js 布局系统的核心价值:
| 特性 | 价值 |
|---|---|
| 嵌套布局 | 自然表达页面层级关系 |
| 状态保持 | 导航时 UI 状态不丢失 |
| 加载状态 | 自动的 Suspense 边界 |
| 并行路由 | 同时渲染多个独立内容 |
| 拦截路由 | 优雅的模态框实现 |
掌握这套系统,能大幅减少布局相关的样板代码,让路由结构清晰可维护。关键是理解每个概念的适用场景,不要过度使用高级特性。
相关文章推荐:


