第 158 章:Next.js App Router 深入
学习目标
- 深入理解 App Router 约定
- 掌握 layout / loading / error 特殊文件
- 学会路由组 (group)
- 理解平行路由与拦截路由
一、App Router 目录约定
二、特殊文件
| 文件 | 作用 |
|---|---|
layout.tsx | 共享布局(嵌套) |
page.tsx | 路由 UI |
loading.tsx | 加载 UI(基于 Suspense) |
error.tsx | 错误边界 |
not-found.tsx | 404 UI |
template.tsx | 每次导航都重新挂载 |
default.tsx | 平行路由的 fallback |
三、layout 嵌套
tsx
// app/layout.tsx(根布局,必须包含 html/body)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
// app/dashboard/layout.tsx(只包裹 dashboard)
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
);
}四、loading 加载状态
tsx
// app/dashboard/loading.tsx
export default function Loading() {
return <div className="p-8">加载中...</div>;
}
// 自动用 React Suspense 包裹 page.tsx五、error 错误边界
tsx
'use client'; // 错误边界必须是 Client Component
// app/dashboard/error.tsx
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>出错了</h2>
<p>{error.message}</p>
<button onClick={reset}>重试</button>
</div>
);
}六、not-found 404
tsx
// app/not-found.tsx
import Link from 'next/link';
export default function NotFound() {
return (
<div className="p-8 text-center">
<h2>404 - 页面未找到</h2>
<Link href="/">回首页</Link>
</div>
);
}七、路由组 (group)
用 (folder) 命名的目录不影响 URL,只用于组织代码和共享布局。
app/
├── (marketing)/
│ ├── layout.tsx # 营销页布局
│ ├── about/
│ │ └── page.tsx # /about
│ └── pricing/
│ └── page.tsx # /pricing
├── (app)/
│ ├── layout.tsx # 应用页布局
│ ├── dashboard/
│ │ └── page.tsx # /dashboard
│ └── settings/
│ └── page.tsx # /settings八、动态路由
8.1 单段动态
app/blog/[slug]/page.tsx → /blog/hello-worldtsx
// app/blog/[slug]/page.tsx
export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
return <h1>文章:{slug}</h1>;
}8.2 多段动态
app/shop/[category]/[id]/page.tsx → /shop/phone/123tsx
export default async function Product({ params }: { params: Promise<{ category: string; id: string }> }) {
const { category, id } = await params;
return <h1>{category} - {id}</h1>;
}8.3 Catch-all
app/docs/[...slug]/page.tsx → /docs/a/b/c/dtsx
export default async function Doc({ params }: { params: Promise<{ slug: string[] }> }) {
const { slug } = await params;
return <pre>{slug.join('/')}</pre>;
}九、Link 与导航
tsx
import Link from 'next/link';
// 基础
<Link href="/about">关于</Link>
// 动态
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
// prefetch(默认自动)
<Link href="/dashboard" prefetch={false}>仪表盘</Link>
// 替换
<Link href="/login" replace>登录</Link>useRouter
tsx
'use client';
import { useRouter } from 'next/navigation';
function Component() {
const router = useRouter();
return <button onClick={() => router.push('/about')}>跳转</button>;
}十、usePathname 与 useSearchParams
tsx
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
function NavItem() {
const pathname = usePathname();
const params = useSearchParams();
const isActive = pathname === '/dashboard';
const filter = params.get('filter');
return <div>{pathname} - {filter}</div>;
}十一、redirect 与 notFound
tsx
import { redirect, notFound } from 'next/navigation';
// Server Component
async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await fetchPost(id);
if (!post) notFound(); // 渲染 not-found.tsx
if (!user) redirect('/login'); // 重定向
return <div>{post.title}</div>;
}十二、本章小结
| 概念 | 关键 |
|---|---|
| 特殊文件 | layout / page / loading / error |
| 路由组 | (group) 不影响 URL |
| 动态路由 | [slug] [...slug] |
| 导航 | Link useRouter |
| 重定向 | redirect notFound |
动手练习
- 实现一个
(marketing)和(app)路由组,不同布局 - 给
/dashboard加 loading.tsx 和 error.tsx - 实现
/blog/[slug]动态路由 - 实现 404 页面
推荐阅读
下一章:第 159 章:路由与布局 →