CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Next.js 实战入门/课时/App Router 与文件路由
RawBlame
课时目录12 节

Next.js 实战入门

  1. 01Next.js 是什么
  2. 02项目创建与目录
  3. 03App Router 与文件路由
  4. 04Server 与 Client 组件
  5. 05数据获取与缓存
  6. 06渲染模式 SSR / SSG / ISR
  7. 07Route Handlers
  8. 08MDX 与课时渲染
  9. 09Tailwind 与样式
  10. 10认证与登录门控
  11. 11读 platform 源码
  12. 12综合练习与部署

App Router 与文件路由

page/layout、动态段 [slug]、notFound、Link。

35 分钟

App Router 与文件路由

这一课你将能做到

  1. 看到任意 URL 就能反推出它在 app/ 下的文件路径,反之亦然
  2. 使用动态段 [slug] 写出像本站课时页 /tracks/[slug]/lessons/[lessonSlug] 这样的嵌套动态路由
  3. 用 layout.tsx 搭建跨页面共享的导航栏,并用 Link、useRouter 实现跳转

为什么要学

路由是 Web 框架的第一公民。Next.js App Router 的路由方案是「文件即路由」——你在文件夹里建 page.tsx,URL 就出现了。这带来两个直接好处:一是零配置,二是看 URL 就能猜到代码位置,这在协作和读源码时价值巨大。本站课时页的 URL 是 /tracks/nextjs-essentials/lessons/next-intro,你读完这课,应该立刻知道它对应 src/app/tracks/[slug]/lessons/[lessonSlug]/page.tsx 这个文件。

面试中路由题是必考基础题:「动态路由怎么写、参数怎么取」「layout 和 page 的区别」「路由跳转的几种方式」。这些题不难,但答得利索能快速建立「写过 Next.js 项目」的印象。

先建立图像

URL 与文件的对照表,先花 30 秒记一遍:

URL文件路径说明
/app/page.tsx首页
/aboutapp/about/page.tsx静态段
/tracksapp/tracks/page.tsx列表页
/tracks/java-essentialsapp/tracks/[slug]/page.tsx动态段
/tracks/java/lessons/introapp/tracks/[slug]/lessons/[lessonSlug]/page.tsx嵌套动态段
/docs/a/b/capp/docs/[...rest]/page.tsx全捕获段
/api/healthapp/api/health/route.ts接口(第 7 课)

三条命名规则:

  • [name] 动态段:匹配任意一段,如 [slug] 匹配 java-essentials
  • (group) 路由组:括号名不出现在 URL 里,仅用于组织文件
  • [...name] 全捕获:匹配剩余所有段(含多段)

嵌套布局的心智图:

app/
├── layout.tsx          ← 所有页面都套这层(根布局,必须有)
├── tracks/
│   ├── layout.tsx      ← /tracks 下所有页面再套这层
│   ├── page.tsx        ← /tracks
│   └── [slug]/
│       └── page.tsx    ← /tracks/java-essentials

跟着做

步骤 1:搭建一个迷你课程站骨架

沿用上一课的项目,创建以下文件:

src/app/
├── layout.tsx          (已存在,加导航栏)
├── page.tsx            (首页)
├── tracks/
│   ├── page.tsx        (课程列表)
│   └── [slug]/
│       └── page.tsx    (课程详情)

步骤 2:根布局加导航栏

修改 src/app/layout.tsx:

import type { Metadata } from "next";
import Link from "next/link";
import "./globals.css";

export const metadata: Metadata = { title: "迷你课程站" };

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="zh-CN">
      <body>
        <nav className="flex gap-4 border-b p-4">
          <Link href="/">首页</Link>
          <Link href="/tracks">课程</Link>
        </nav>
        {children}
      </body>
    </html>
  );
}

要点:站内跳转必须用 next/link 的 <Link>,它会预取目标页面并做客户端导航(不整页刷新)。写 <a href> 虽然也能跳,但会丢失 SPA 体验。

步骤 3:列表页 + 编程式跳转

src/app/tracks/page.tsx:

import Link from "next/link";

const tracks = [
  { slug: "java-essentials", title: "Java 基础" },
  { slug: "nextjs-essentials", title: "Next.js 基础" },
];

export default function TracksPage() {
  return (
    <main className="p-6">
      <h1 className="text-2xl font-bold">全部课程</h1>
      <ul className="mt-4 space-y-2">
        {tracks.map((t) => (
          <li key={t.slug}>
            <Link className="text-blue-600 hover:underline" href={`/tracks/${t.slug}`}>
              {t.title}
            </Link>
          </li>
        ))}
      </ul>
    </main>
  );
}

步骤 4:动态路由详情页——重点

src/app/tracks/[slug]/page.tsx。注意 Next 15 起 params 是 Promise,必须 await(Next 14 中是同步对象,本课程按 15 的新写法,向下兼容提醒会标注):

import { notFound } from "next/navigation";

const tracks = [
  { slug: "java-essentials", title: "Java 基础", desc: "面向零基础" },
  { slug: "nextjs-essentials", title: "Next.js 基础", desc: "App Router 全流程" },
];

interface PageProps {
  params: Promise<{ slug: string }>;
}

export default async function TrackPage({ params }: PageProps) {
  const { slug } = await params;
  const track = tracks.find((t) => t.slug === slug);

  if (!track) notFound(); // 渲染最近的 not-found.tsx 或默认 404

  return (
    <main className="p-6">
      <h1 className="text-2xl font-bold">{track.title}</h1>
      <p className="mt-2 text-gray-600">{track.desc}</p>
    </main>
  );
}

访问 /tracks/java-essentials 出详情,访问 /tracks/不存在 出 404——notFound() 就是这么用。

步骤 5:再嵌一层动态段

新建 src/app/tracks/[slug]/lessons/[lessonSlug]/page.tsx:

interface PageProps {
  params: Promise<{ slug: string; lessonSlug: string }>;
}

export default async function LessonPage({ params }: PageProps) {
  const { slug, lessonSlug } = await params;
  return (
    <main className="p-6">
      <h1 className="text-xl font-bold">课程:{slug}</h1>
      <h2 className="mt-1 text-lg text-gray-700">课时:{lessonSlug}</h2>
    </main>
  );
}

访问 /tracks/nextjs-essentials/lessons/next-intro,两段动态参数都拿到了。本站真实的课时页就是这个结构,多了一个查询登录态、渲染 MDX 的步骤而已。

步骤 6:客户端组件里用 useRouter 跳转

动态交互(点按钮跳转)在 Client 组件里做:

"use client";

import { useRouter } from "next/navigation";

export default function GoButton() {
  const router = useRouter();
  return (
    <button
      className="rounded bg-blue-600 px-4 py-2 text-white"
      onClick={() => router.push("/tracks")}
    >
      去课程列表
    </button>
  );
}

router.push 前进、router.replace 替换历史记录(登录后回跳常用)、router.back 后退、router.refresh 让服务端重新渲染当前页。

讲清楚要点

  1. page.tsx 是路由的「最小单元」:文件夹只是命名空间,没有 page.tsx 的文件夹不产生 URL。这让「共用布局」和「路由」解耦——布局挂在文件夹上,页面才挂 URL。
  2. layout 层层嵌套且保留状态:从根到当前页的所有 layout 同时渲染。用户在 /tracks/a 与 /tracks/b 之间跳转时,tracks/layout.tsx 不会重新渲染,里面的状态保留——这就是「布局只在段变化时重渲染」的官方表述。
  3. params(路径参数)与 searchParams(查询参数)要分清:/tracks/[slug] 的 slug 来自路径,是路由的一部分;/tracks?page=2 的 page 来自 ? 后面,叫 searchParams,同样是 Promise:const { page } = await searchParams。注意:页面里读了 searchParams 会强制该页走动态渲染(第 6 课细讲)。
  4. 三种跳转方式按场景选:内容可点击(列表项、卡片)用 <Link>;提交表单后跳转、登录成功后跳转用 router.push/replace;服务端逻辑里重定向(如未登录访问)用 redirect("/login")(从 next/navigation 导入,在 Server Component 或 Server Action 里调用)。
  5. 路由组 (group) 解决「不同板块不同布局」:比如 (shop)/cart/page.tsx 与 (blog)/post/page.tsx,URL 都是顶层 /cart、/post,但可以各自套不同 layout,互不影响。

你可能会懵的地方

  • Next 14 和 15 在 params 上的区别到底改了什么? 14 里 params 是普通对象,直接 params.slug;15 里改成 Promise(为支持流式渲染前先出壳),统一 await params。本站源码里 params: Promise<{...}> 就是 15 的写法。写 14 的项目照旧语法即可,两种写法不混用就不会错。

  • 动态段的值会变吗?URL 改名了路由也变吗? 动态段匹配的是「值」,不是「路由」。/tracks/[slug] 一个文件就能服务无穷多个 /tracks/xxx。这也是为什么本站新增课程不用改任何路由代码。

  • [...slug] 和 [[...slug]] 的区别? [...slug] 至少要有一段(/docs/a 起);[[...slug]] 可选捕获,空的时候也匹配(/docs 本身)。一般文档站根路径用可选捕获。

  • 为什么推荐 Link 而不是 a 标签,哪怕都是跳转? Link 做客户端导航:不发整页请求、预取可视区域内的链接、保留滚动与状态。a 标签触发完整页面加载,等于放弃 Next.js 的性能优化。站外链接才用 a 标签。

  • 404 页面在哪定义? 在对应层级建 not-found.tsx(根级 app/not-found.tsx 兜底),里面正常写 JSX,调用 notFound() 的组件会渲染到最近的 not-found 边界。同理还有 error.tsx(必须 'use client')与 loading.tsx。

错法对照

错法问题正解
src/app/tracks/JavaEssentials/page.tsx 想匹配大写 URL文件夹名区分大小写,Windows 本地能跑、Linux 生产 404段名统一 kebab-case 小写
页面里直接 params.slug(Next 15)TS 报错 / 运行时 undefinedconst { slug } = await params
把接口写在 app/api/x/page.tsx接口必须叫 route.tsapp/api/x/route.ts(第 7 课)
登录后用 router.push 跳走,用户按后退又回登录页历史记录残留用 router.replace("/me")
在根布局里读 searchParams根布局拿不到 searchParams在 page 层读取再传下去

卡点急救

  1. 动态路由刷新后 404:多半在静态部署(如 output: "export" 纯静态导出)下没生成兜底。服务器模式(npm run start、Vercel)不存在此问题;确认你不是在用纯静态托管直接刷新动态段。
  2. Link 点击没反应:检查 href 是否为完整字符串(不要嵌套 <a> 在 <Link> 里);若用了自定义组件包裹 Link,把交互逻辑放进子组件而不是改 Link 本身。
  3. await params 后 TS 还报 possibly undefined:params 的键来自文件夹名,拼错键名(如文件叫 [id] 却解构 slug)拿到的就是 undefined。对一下文件夹名。

面试常问

  • App Router 中 layout 和 page 的关系与区别? page 定义一个路由的 UI,layout 包裹其下所有段。布局跨页面持久:段间导航时 layout 不重渲染、状态保留;page 每次导航都会重新渲染。layout 可以嵌套(根 layout 必须有,含 html/body),还能有 loading/error/not-found 各级边界。

  • 动态路由参数如何获取?14 与 15 的差异? 在页面 props 上接收 params。Next 15 起 params/searchParams 均为 Promise,需 await 后解构,这使 Next 能先渲染骨架再等待参数数据(配合流式渲染)。路径参数来自 [slug] 文件夹,查询参数来自 URL ? 后的部分,两者都通过 await 获取。

  • 服务端重定向和客户端跳转分别怎么做? 服务端:Server Component 或 Server Action 里 redirect("/login")(抛出特殊错误由框架处理,可用于鉴权门控)。客户端:useRouter().push/replace/back;声明式用 <Link>。表单提交后的跳转用 replace 避免后退回到表单页。

自测三问

  1. /tracks/[slug]/lessons/[lessonSlug] 能匹配哪些 URL?参数怎么取?
  2. 用户从 /tracks/a 跳到 /tracks/b,tracks/layout.tsx 会重新执行吗?为什么这很重要?
  3. <Link>、router.push、redirect 分别用在什么场景?

小结一句话

文件即路由:page.tsx 生成 URL、[slug] 接参数、layout.tsx 层层包裹且跨页持久,跳转三件套是 Link / router / redirect。

延伸

  • Next.js 官方文档:Routing
  • 下一课:本 track 最重要的一课——Server 与 Client 组件。
加载本节练习…
← 上一课项目创建与目录下一课 →Server 与 Client 组件