2.2 组件与布局(Components & Layouts)


2.2 组件与布局(Components & Layouts)

本节摘要:页面是组件拼出来的,布局决定页面的"骨架"。本节讲清组件组织的最佳实践(页面组件、功能组件、UI 组件分层)、嵌套布局与模板的区别,以及 loading.tsx 与 error.tsx 两个提升体验的内置边界。

本节地图

阅读完本节,你应当能够:

  1. 按职责组织组件(页面/功能/UI 三层)
  2. 用嵌套布局构建应用骨架
  3. 理解 template.tsx 与 layout.tsx 的区别
  4. 用 loading.tsx 做加载状态
  5. 用 error.tsx 做错误边界

问题与直觉:组件放哪,决定了项目好不好维护

项目一大会发现:所有组件堆在 components/ 里,命名靠猜、职责混杂。组件组织的本质是按"变化频率"与"职责边界"分层——页面组件随路由变、功能组件随业务变、UI 组件几乎不变。分层清晰后,改一个按钮样式不会误伤业务逻辑。

直觉类比:组件分层像"饭店的岗位分工"——UI 组件是"餐具"(通用,所有菜用),功能组件是"特色菜师傅"(负责一类业务),页面组件是"传菜员"(把菜端到对应桌)。各司其职,互不越界。

💡 关键直觉:布局(layout)是"不会随路由切换而重新渲染的骨架"——导航栏、侧边栏、页脚放布局里,切换页面时它们保持状态、不闪跳。这是 Next.js 与"整页刷新"式 MPA 的体验分水岭。

核心原理:布局与边界文件

2.1 嵌套布局

每个目录可有自己的 layout.tsx,自动嵌套包裹:

app/ ├── layout.tsx # 根布局:html/body/全局导航 ├── (shop)/ │ ├── layout.tsx # 商城布局:侧边栏/购物车 │ ├── products/page.tsx # /products │ └── cart/page.tsx # /cart └── blog/ └── layout.tsx # 博客布局:目录/标签云

切换路由时的行为/products/cart,商城布局不重新渲染(购物车状态保留);跨布局切换(商城 → 博客)才重新渲染。

2.2 template.tsx:每次切换都重建

与 layout 相对,template 在每次导航时重新创建(重置状态):

// app/blog/template.tsx —— 每次进入博客路由都重新挂载 export default function BlogTemplate({ children }) { return <div className="blog-template">{children}</div>; }

layout vs template:layout 保留状态(导航/鉴权框)、template 重置状态(统计埋点、动画容器)。99% 的场景用 layout。

2.3 loading.tsx:加载态

同目录下放 loading.tsx,路由切换时立即显示(流式渲染,不等数据):

// app/posts/loading.tsx export default function Loading() { return <div className="animate-pulse">加载文章列表...</div>; }

原理:Next.js 把页面拆成"布局(立即显示)+ 页面内容(数据就绪后流入)"。loading.tsx 就是"数据就绪前先显示的占位",提升感知速度

2.4 error.tsx:错误边界

"use client"; // error 必须是客户端组件 export default function Error({ reset }: { reset: () => void }) { return ( <div> <h2>页面出错了</h2> <button onClick={reset}>重试</button> </div> ); }

与全局 404 的区别:error.tsx 处理运行时错误(数据获取失败、组件抛错);not-found.tsx 处理 404(资源不存在)。

工程实践要点:组件组织规范

3.1 三层组件结构

components/ ├── ui/ # UI 基础组件(Button、Card、Input) │ └── Button.tsx ├── features/ # 功能组件(绑定业务逻辑) │ ├── auth/LoginForm.tsx │ └── posts/PostCard.tsx └── layouts/ # 布局组件 └── Header.tsx

分层规则

  • UI 组件:纯展示、无业务数据、可复用;
  • 功能组件:绑定某个业务域,组合 UI 组件 + 数据;
  • 页面组件:app/ 目录下的 page.tsx,组织功能组件与布局。

3.2 组件与服务器/客户端边界结合

// 服务器组件包一层(取数),客户端组件做交互 // app/posts/page.tsx(服务器) import { PostCard } from "@/components/features/posts/PostCard"; export default async function PostsPage() { const posts = await getPosts(); return ( <div className="grid gap-4"> {posts.map((p) => <PostCard key={p.id} post={p} />)} </div> ); } // components/features/posts/PostCard.tsx(客户端) "use client"; export function PostCard({ post }: { post: Post }) { const [liked, setLiked] = useState(false); return ( <article> <h2>{post.title}</h2> <button onClick={() => setLiked(!liked)}>赞</button> </article> ); }

职责分配:数据获取在服务器组件层,交互在客户端组件层,UI 组件纯展示——三层各司其职。

3.3 组合模式:服务器组件作为"容器"

一个实用模式:服务器组件取数 + 传 props 给客户端子组件(已序列化数据),客户端组件只做交互——避免客户端组件自己取数导致的"加载两次"

常见误区与排查

误区 现象 正解
所有组件塞一个目录 找不到、命名冲突 按 ui/features/layouts 分层
导航栏放页面而非布局 切换页面导航闪跳 放 layout.tsx
loading 不生效 页面还是空白等待 loading.tsx 放同目录,配合流式渲染
error.tsx 没用 use client 报错 错误边界必须是客户端组件
layout 与 template 混用 状态行为不可预期 默认用 layout,特殊需求用 template

动手演练:带加载态的博客布局

// 1. app/blog/layout.tsx —— 博客布局(保留状态) export default function BlogLayout({ children }) { return ( <div className="flex gap-6"> <aside className="w-48">目录导航</aside> <main>{children}</main> </div> ); } // 2. app/blog/loading.tsx —— 加载占位 export default function Loading() { return <div>加载中...</div>; } // 3. app/blog/error.tsx —— 错误边界 "use client"; export default function Error({ reset }) { return ( <div> <p>博客加载失败</p> <button onClick={reset}>重试</button> </div> ); }

访问 /blog 时:布局立即显示(目录导航不闪),内容区先显示 loading 再流入数据;出错时显示重试按钮——布局、加载、错误三种状态都得到了优雅处理

要点串联

  • 嵌套布局:layout.tsx 包裹子路由,切换路由不重新渲染、保留状态。
  • template:每次导航重建(重置状态),99% 用 layout。
  • loading.tsx:流式渲染的占位,提升感知速度。
  • error.tsx:运行时错误边界,必须是客户端组件;与 not-found(404)区分。
  • 三层组件:ui(纯展示)/ features(业务绑定)/ 页面组件(组织编排)。
  • 组合模式:服务器组件取数,客户端组件交互,UI 组件纯展示。

深入理解:布局与边界的组合模式

嵌套布局的完整结构

要点:布局是"树"结构——根布局包裹所有,子布局只包裹自己的路由段。切换同布局内的页面(商品 → 购物车),商城布局不重新渲染;跨布局(商城 → 博客)才重建。

loading 与 error 的嵌套行为

  • loading:每个路由段可有自己的 loading,导航时"就近显示"(最内层先显示);
  • error:错误向上冒泡,最近的 error.tsx 捕获;根级错误用 global-error.tsx;
  • 组合布局(立即显示)→ loading(数据等待)→ 页面(数据就绪)——三层渐进体验。

组件边界再确认

生产建议:一个页面的默认形态是"服务器组件为主体,客户端组件做交互叶子"。遇到"这个页面很卡"时,先检查是否客户端组件过多——把非交互部分移回服务器组件,往往立竿见影。

常见问题速答

问:loading.tsx 在什么情况下触发?
路由切换且目标页面的数据(服务器组件取数)尚未完成时,loading.tsx 显示。它配合流式渲染——布局立即显示,loading 占位,数据就绪后流入。

问:error.tsx 和 not-found.tsx 什么时候用哪个?
error 处理运行时错误(取数失败、组件抛错);not-found 处理资源不存在(404)。两者可同时存在,各自负责一类情况。

问:布局的嵌套层级有限制吗?
没有硬性限制,但建议不超过 3-4 层——层级越深,导航时重建的布局越多,性能与维护成本越高。

问:template 有什么典型用途?
需要"每次进入都重置状态"的场景:页面浏览统计(每次进入上报)、动画容器(每次进入重新播放)。大多数场景用 layout 就够。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U