布局与页面
Next.js 基于文件系统路由:文件夹决定 URL 段,page.tsx 和 layout.tsx 决定每段对应的 UI。本章讲清楚二者如何协作,以及动态段和 searchParams 的用法。
创建页面
在 app 下放一个 page.tsx 并默认导出组件,这个路由就变得公开可访问:
// app/page.tsx → /
export default function Page() {
return <h1>Hello Next.js!</h1>;
}
创建布局
布局是多个页面共享的 UI。在客户端导航时布局会保留状态、保持交互性,且不会重新渲染。
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body>
<main>{children}</main>
</body>
</html>
);
}
位于 app/ 根部的叫 根布局,是必须的,而且只有它能写 <html> 和 <body>。
嵌套路由与嵌套布局
嵌套文件夹 = 嵌套 URL 段。例如 /blog/[slug] 对应:
app/
├── layout.tsx # 根布局
├── blog/
│ ├── layout.tsx # 博客区布局(可选)
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/hello-world
布局会自动嵌套包裹:根布局 → 博客布局 → 博客页。这样可以在各层按需复用导航、侧栏、面包屑等结构。
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
return <section className="blog">{children}</section>;
}
动态段
用方括号创建动态段:[slug]、[id]。Next.js 15+ 的 params 是一个 Promise,需要 await:
// app/blog/[slug]/page.tsx
export default async function Post({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
配合 generateStaticParams 可以在构建时预生成所有动态路由,获得纯静态站点的性能。
searchParams
在服务端组件页面中通过 searchParams 读取查询参数:
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ [k: string]: string | string[] | undefined }>;
}) {
const { q } = await searchParams;
return <p>搜索词:{q}</p>;
}
注意:使用
searchParams会让页面自动变成 动态渲染,因为它依赖进来的请求。
客户端组件可以用 useSearchParams 这个 hook。
页面间导航
使用 next/link 实现客户端导航(带自动预加载):
import Link from 'next/link';
export default async function Posts() {
const posts = await getPosts();
return (
<ul>
{posts.map(p => (
<li key={p.slug}>
<Link href={`/blog/${p.slug}`}>{p.title}</Link>
</li>
))}
</ul>
);
}
下一章我们会深入讲解 Link 的预取、流式渲染与客户端过渡。