Skip to content
第 21 章 前端 ⏱ 13 分钟阅读

第 21 章:Next.js 路由 ​

学习目标 ​

  • 掌握 App Router 的文件约定
  • 熟练使用动态路由、嵌套布局
  • 学会 Parallel Routes 和 Loading UI
  • 避开 Next.js 路由的 4 个常见坑

一、App Router 文件约定 ​

文件作用
page.tsx路由的 UI
layout.tsx共享布局(可嵌套)
loading.tsx加载中 UI(自动 Suspense)
error.tsx错误边界
not-found.tsx404 页面
route.tsAPI 端点
template.tsx每次导航重新挂载的布局
app/
├── layout.tsx         # 根布局(必需)
├── page.tsx           # /
├── about/
│   └── page.tsx       # /about
├── blog/
│   ├── page.tsx       # /blog
│   └── [slug]/
│       └── page.tsx   # /blog/xxx
└── dashboard/
    ├── layout.tsx     # 嵌套布局
    ├── page.tsx       # /dashboard
    └── settings/
        └── page.tsx   # /dashboard/settings

二、动态路由 ​

tsx
// app/blog/[slug]/page.tsx
interface Props { params: { slug: string }; }

export default function Post({ params }: Props) {
  return <h1>文章:{params.slug}</h1>;
}

// app/shop/[...slug]/page.tsx    捕获所有段
// /shop/a/b/c → params.slug = ['a', 'b', 'c']

// app/shop/[[...slug]]/page.tsx  可选捕获
// /shop 或 /shop/a/b 都匹配

⚠️ 坑 1:Next.js 14 中 params 是同步对象,Next.js 15 起变 Promise(params: Promise<...>),用 await params。

三、嵌套布局 ​

tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex">
      <aside className="w-64 bg-gray-100">
        <Sidebar />
      </aside>
      <main className="flex-1 p-8">{children}</main>
    </div>
  );
}

布局自动嵌套,所有 /dashboard/* 页面共享侧边栏。

💡 layout.tsx 是 Next.js 框架约定的特殊文件名——看到这几个文件名,Next.js 就知道干啥:约定大于配置。

特殊文件清单:

文件名作用
layout.tsx布局(自动嵌套到子路由)
page.tsx页面内容
loading.tsx加载中(自动包裹)
error.tsx错误页(自动捕获)
not-found.tsx404

嵌套工作原理(俄罗斯套娃):

访问 /dashboard 时,Next.js 自动嵌套:
RootLayout (app/layout.tsx)
  └─ DashboardLayout (app/dashboard/layout.tsx)
      └─ Page (app/dashboard/page.tsx)

{children} 是 Next.js 自动传的子页面——你布局里写 {children},Next.js 把 page.tsx 塞进去:

tsx
function DashboardLayout({ children }) {
  return (
    <div>
      <Sidebar />
      {children}    ← 这里渲染 page.tsx 的内容
    </div>
  )
}

跟 Vue 对比:Vue <router-view /> ≈ Next.js {children}——本质都是布局写外面,内容塞中间。

常见项目结构:

app/
├─ layout.tsx                ← 全站(html/body)
├─ page.tsx                  ← 首页
├─ (auth)/                   ← 路由组(不影响 URL)
│   ├─ login/page.tsx
│   └─ layout.tsx            ← 登录页布局(居中卡片)
└─ dashboard/
    ├─ layout.tsx            ← 带侧边栏的布局
    ├─ page.tsx              ← /dashboard
    ├─ settings/page.tsx     ← /dashboard/settings(共享侧边栏)
    └─ user/[id]/page.tsx    ← /dashboard/user/123(也共享侧边栏)

四、Loading UI ​

tsx
// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="space-y-4">
      <div className="h-8 bg-gray-200 animate-pulse rounded" />
      <div className="h-32 bg-gray-200 animate-pulse rounded" />
    </div>
  );
}

loading.tsx 自动被包在 Suspense 里,流式渲染显示骨架屏。

💡 loading.tsx 跟 layout.tsx 一样按目录层级生效——内层覆盖外层,越具体优先级越高——写在 app/blog/loading.tsx 只对 /blog/* 生效。

五、Error Boundary ​

tsx
'use client';     // �️ error.tsx 必须是 Client Component

import { useEffect } from 'react';

export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  useEffect(() => { console.error(error); }, [error]);

  return (
    <div>
      <h2>出错了!</h2>
      <button onClick={reset}>重试</button>
    </div>
  );
}

⚠️ 坑 2:error.tsx 必须是 'use client',但它捕获的错误可以来自服务端组件。

tsx
import Link from 'next/link';
import { useRouter } from 'next/navigation';     // ⚠️ App Router 用 next/navigation

function Nav() {
  return (
    <nav>
      <Link href="/">首页</Link>
      <Link href="/about" prefetch={false}>关于</Link>   {/* 关闭预取 */}
    </nav>
  );
}

function GoBack() {
  const router = useRouter();
  return <button onClick={() => router.back()}>返回</button>;
}

⚠️ 坑 3:App Router 用 next/navigation,不是 next/router(那是 Pages Router 的)。

七、usePathname / useSearchParams ​

tsx
'use client';

import { usePathname, useSearchParams } from 'next/navigation';

function Header() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const query = searchParams.get('q');

  return <div>当前:{pathname}?q={query}</div>;
}

⚠️ 坑 4:usePathname 等 hooks 只能在 Client Component 用。

💡 动态路由三种写法:

写法匹配例子
[id]一个段(必填)/blog/123
[[slug]]一个段(可选)/blog 或 /blog/abc
[...slug]一个或多个段(rest)/docs/a/b/c(返回数组)

典型场景:

tsx
// ① [id]:必传参数
// app/blog/[id]/page.tsx
// /blog/123 ✅ → params.id = '123'
// /blog     ❌ → 404

// ② [[slug]]:可选参数(/blog 和 /blog/xxx 都能访问)
// app/blog/[[slug]]/page.tsx
// /blog/abc ✅ → params.slug = 'abc'
// /blog     ✅ → params.slug = undefined(不传也行)

// ③ [...slug]:捕获所有(返回数组)
// app/docs/[...slug]/page.tsx
// /docs/a/b/c ✅ → params.slug = ['a', 'b', 'c']
// /docs       ❌ → 404

💡 记忆口诀:[id] 必填一个,[[slug]] 可选一个,[...slug] 捕获多个——多一层括号 = "可选"。

八、Route Groups(分组不写入 URL) ​

用 (name) 包裹,不参与路径:

app/
├── (marketing)/
│   ├── page.tsx          # /
│   └── about/page.tsx    # /about
└── (app)/
    ├── dashboard/page.tsx  # /dashboard
    └── settings/page.tsx   # /settings

用途:

  • 不同布局(营销页 vs 后台)
  • 逻辑分组而不影响 URL

💡 Next.js App Router = 文件目录即路由——没有 routes 配置——page.tsx 是页面、[id] 是动态、(group) 是分组不影响 URL。目录长啥样,URL 就长啥样。

九、Parallel Routes(并行路由) ​

tsx
// app/dashboard/layout.tsx
export default function Layout({
  children,
  team,
  analytics,
}: {
  children: React.ReactNode;
  team: React.ReactNode;
  analytics: React.ReactNode;
}) {
  return (
    <div className="grid grid-cols-2 gap-4">
      <div>{team}</div>
      <div>{analytics}</div>
      <div className="col-span-2">{children}</div>
    </div>
  );
}

目录:

app/dashboard/
├── layout.tsx
├── page.tsx              # children
├── @team/
│   └── page.tsx
└── @analytics/
    └── page.tsx

适用:仪表盘多面板、独立加载。

十、本章小结 ​

要点关键
page.tsx路由 UI
layout.tsx嵌套布局,自动包裹子路由
loading.tsxSuspense 自动包装
error.tsx错误边界,必须是 Client
动态路由[slug]、[...catchAll]、[[...opt]]
导航next/navigation(不是 next/router)
Route Group(name) 分组不影响 URL
Parallel@slot 多面板并行渲染

动手练习 ​

  1. 博客路由:/blog/[slug],显示文章 ID
  2. 嵌套布局:/dashboard 套侧边栏,内部子页面共享
  3. loading/error:为 /blog 加 loading.tsx 和 error.tsx
  4. 动态元数据:用 generateMetadata 根据 slug 动态生成 title

下一章:第 22 章:Next.js 数据获取 →

本站基于 VitePress 构建 · 由 StackHub 团队维护