N03第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

布局、嵌套与导航

布局、嵌套与导航 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

布局是「页面之间不变的那部分」。读完你应该能建立根布局和学习区嵌套布局,验证页面切换时布局不重载,并用 usePathname 做出当前路由高亮的导航。

问题场景

学习工作台顶部有全局导航(课程 / 练习 / 关于),学习区还有一个侧边栏(目录、进度)。如果每个页面都复制粘贴导航,改个 logo 要动所有页面;如果导航是普通组件,页面切换时它会被卸载重建——正在播放的音频、滚动位置、打开的下拉菜单都会丢。

核心解释

App Router 用 layout.tsx 解决:布局包裹子路由,页面切换时只替换 children,布局本身不重渲染。

// app/layout.tsx —— 根布局,必须有 <html> 和 <body>
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <body>
        <SiteNav />          {/* 全局导航,任何页面切换都不重载 */}
        {children}
      </body>
    </html>
  );
}

嵌套布局建在子目录里,自动包裹该目录下所有页面:

// app/learn/layout.tsx —— /learn 下的页面共用
export default function LearnLayout({ children }: { children: React.ReactNode }) {
  return (
    <div style={{ display: "flex" }}>
      <aside>学习区侧边栏</aside>
      <section>{children}</section>
    </div>
  );
}

布局与页面的两个重要性质:

  • 布局保留:从 /learn/a 导航到 /learn/b,只有 LearnLayout 的 children 变化,布局组件的 state、滚动位置原样保留——这就是把状态类组件放布局里的底气;
  • 错误隔离:某页面抛错,由它所在目录的 error.tsx 兜住,布局和导航仍然完好;布局本身报错才会向外冒泡到上一级。

导航高亮需要「当前路径」,而布局默认是服务端组件拿不到 pathname。官方姿势是一个 "use client" 的小导航组件:

// components/site-nav.tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";

const LINKS = [
  { href: "/courses", label: "课程" },
  { href: "/learn", label: "学习区" },
];

export function SiteNav() {
  const pathname = usePathname();
  return (
    <nav>
      {LINKS.map(l => (
        <Link key={l.href} href={l.href} aria-current={pathname.startsWith(l.href) ? "page" : undefined}>
          {l.label}
        </Link>
      ))}
    </nav>
  );
}

注意职责划分:交互(读 pathname)收在客户端小组件里,布局仍然是服务端组件——这正是 N05 要展开的边界思维。

示例拆解

本课落地的文件与验证:

  • app/layout.tsx:根布局,挂 SiteNav 和全局样式;
  • app/learn/layout.tsx:学习区布局,侧边栏 + children;
  • components/site-nav.tsx:usePathname 高亮当前项;
  • 验证一:在 /learn 下两个页面间切换,侧边栏里放一个 useState 计数器(点一下加一),导航后计数不清零——布局状态保留的直接证据;
  • 验证二:让某个 learn 页面抛错,观察侧边栏和顶部导航完好,只有内容区显示错误兜底。

动手任务

在项目里完成三个小改动:

  1. 建根布局和 /learn 嵌套布局,SiteNav 放进根布局。
  2. 实现 usePathname 高亮,给当前项加下划线或 aria-current。
  3. 在 LearnLayout 侧边栏加一个 useState 计数器,页面间切换验证它不重置;再让一个页面 throw 看错误隔离。

常见错误

  • 根布局缺 <html> 或 <body>:build 直接报错;整个应用只有根布局负责这两个标签。
  • 把 pathname 逻辑写在服务端布局里:服务端组件没有 usePathname;拆一个 use client 的导航子组件出来。
  • 用 useEffect + window.location 做「当前路径」:客户端路由切换不触发页面加载事件,高亮会失灵;用 usePathname 响应路由变化。
  • 以为布局会随数据变化重渲染:布局默认不参与页面数据流,需要共享的全局数据要么放 Context(客户端),要么在布局里自己获取。

检查与复盘

完成下方检查题确认理解。自查清单:

  • 能说出「children 替换、布局保留」的机制并用状态计数验证过
  • 导航高亮在 use client 组件里用 usePathname 实现
  • 页面级错误不拖垮布局,验证过隔离效果

参考资料

检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1单选简单

课程站有根 layout 和课程区段的 layout。用户从一门课程页导航到另一门课程页时,这两个 layout 会发生什么?

Q2故障定位中等

bug 现场:顶栏有一个「今日学习计时器」,状态和定时器写在 app/template.tsx 里。每次点击侧边栏切换课程,计时都归零重新开始。原因和修法是?

// app/template.tsx
'use client';
export default function Template({ children }) {
  const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    const t = setInterval(() => setSeconds((s) => s + 1), 1000);
    return () => clearInterval(t);
  }, []);
  return (
    <>
      <header>已学习 {seconds} 秒</header>
      {children}
    </>
  );
}

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...