第 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.tsx | 404 页面 |
route.ts | API 端点 |
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二、动态路由
// 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。
三、嵌套布局
// 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 塞进去:tsxfunction 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
// 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
'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',但它捕获的错误可以来自服务端组件。
六、Link 与导航
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
'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(并行路由)
// 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.tsx | Suspense 自动包装 |
| error.tsx | 错误边界,必须是 Client |
| 动态路由 | [slug]、[...catchAll]、[[...opt]] |
| 导航 | next/navigation(不是 next/router) |
| Route Group | (name) 分组不影响 URL |
| Parallel | @slot 多面板并行渲染 |
动手练习
- 博客路由:
/blog/[slug],显示文章 ID - 嵌套布局:
/dashboard套侧边栏,内部子页面共享 - loading/error:为
/blog加 loading.tsx 和 error.tsx - 动态元数据:用
generateMetadata根据 slug 动态生成 title
下一章:第 22 章:Next.js 数据获取 →