news 2026/5/11 2:50:21

Next.js 页面和路由

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Next.js 页面和路由

Next.js 页面与路由学习笔记

Next.js 13+ 的 App Router 基于文件系统路由,通过文件夹和文件的命名约定自动生成路由,无需手动配置路由表。


1. 基本路由规则

1.1 核心约定

文件作用是否必须
page.tsx定义路由的 UI(页面内容)(没有则该路径不可访问)
layout.tsx定义路由的布局(包裹子页面)否(根布局必须)
loading.tsx定义加载状态
error.tsx定义错误边界
not-found.tsx定义 404 页面
template.tsx类似 layout,但导航时重新挂载

1.2 文件夹 = 路由段

app/ ├── page.tsx → / ├── about/ │ └── page.tsx → /about ├── blog/ │ └── page.tsx → /blog │ └── [slug]/ │ └── page.tsx → /blog/hello-world ├── dashboard/ │ ├── page.tsx → /dashboard │ └── settings/ │ └── page.tsx → /dashboard/settings

关键规则

  • 只有存在page.tsx的文件夹才是可访问的路由
  • 文件夹名即为 URL 路径段。
  • page.tsx默认导出的组件就是该路由的页面内容。

2. 动态路由

2.1 基本动态路由[slug]

用方括号包裹的文件夹名表示动态参数。

app/ └── blog/ └── [slug]/ └── page.tsx → /blog/任意字符串
// app/blog/[slug]/page.tsx export default function BlogPost({ params }: { params: { slug: string } }) { return <h1>文章: {params.slug}</h1>; } // 访问 /blog/hello-world → 显示 "文章: hello-world" // 访问 /blog/nextjs-13 → 显示 "文章: nextjs-13"

2.2 多段动态路由

app/ └── shop/ └── [category]/ └── [id]/ └── page.tsx → /shop/shoes/nike-001
// app/shop/[category]/[id]/page.tsx export default function Product({ params, }: { params: { category: string; id: string }; }) { return ( <div> <p>分类: {params.category}</p> <p>商品: {params.id}</p> </div> ); }

2.3 捕获所有路由[...slug]

[...slug]匹配剩余所有路径段(至少匹配一段)。

app/ └── docs/ └── [...slug]/ └── page.tsx
URLparams.slug
/docs/getting-started['getting-started']
/docs/api/reference['api', 'reference']
/docs/guide/advanced/config['guide', 'advanced', 'config']
/docs404(至少需要一段)

2.4 可选捕获所有路由[[...slug]]

用双括号表示可选(0 段或多段都匹配)。

app/ └── docs/ └── [[...slug]]/ └── page.tsx
URLparams.slug
/docsundefined[]
/docs/getting-started['getting-started']
/docs/api/reference['api', 'reference']

3. 布局系统 (Layout)

3.1 根布局 (Root Layout)

必须存在,定义在app/layout.tsx,包含<html><body>标签。

// app/layout.tsx export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="zh"> <body> <header>全局导航栏</header> <main>{children}</main> <footer>全局页脚</footer> </body> </html> ); }

3.2 嵌套布局 (Nested Layout)

子目录中的layout.tsx嵌套在父布局内,不会替换父布局。

app/ ├── layout.tsx ← 根布局(全局导航 + 页脚) ├── page.tsx ← 首页 └── dashboard/ ├── layout.tsx ← 仪表盘布局(侧边栏) ├── page.tsx ← /dashboard └── settings/ └── page.tsx ← /dashboard/settings
// app/dashboard/layout.tsx export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { return ( <div style={{ display: 'flex' }}> <nav>侧边栏:概览 | 设置 | 消息</nav> <section>{children}</section> </div> ); }

渲染结果(访问/dashboard/settings):

<html> <body> <header>全局导航栏</header> ← 根布局 <main> <div style="display:flex"> <nav>侧边栏</nav> ← dashboard 布局 <section> <h1>设置页面</h1> ← settings/page.tsx </section> </div> </main> </body> </html>

3.3 layout vs template

特性layout.tsxtemplate.tsx
实例共享(导航时不重新创建)独立(每次导航重新挂载)
状态保持保持(跨页面共享状态)不保持(每次重置)
性能更好(不重新挂载)稍差
适用场景通用布局进入/离开动画、useEffect每次执行

4. 特殊文件详解

4.1loading.tsx— 加载状态

利用 React Suspense,在页面数据加载时自动显示。

// app/dashboard/loading.tsx export default function Loading() { return ( <div className="flex items-center justify-center h-screen"> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500" /> </div> ); }

效果:访问/dashboard时,如果页面组件正在获取数据,先显示 loading 动画,数据就绪后自动替换为真实内容。

4.2error.tsx— 错误边界

捕获当前路由段的运行时错误,显示友好的错误提示。

// app/error.tsx 'use client'; // 必须是客户端组件 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> ); }

4.3not-found.tsx— 404 页面

当调用notFound()函数或路径不存在时显示。

// app/not-found.tsx export default function NotFound() { return ( <div> <h1>404</h1> <p>页面不存在</p> <a href="/">返回首页</a> </div> ); } // 在页面中主动触发 import { notFound } from 'next/navigation'; export default async function Post({ params }) { const post = await getPost(params.slug); if (!post) { notFound(); // 触发 not-found.tsx } return <article>{post.title}</article>; }

4.4route.ts— API 路由

处理 HTTP 请求,替代传统的 Express 路由。

// app/api/users/route.ts import { NextResponse } from 'next/server'; // GET /api/users export async function GET() { const users = await db.user.findMany(); return NextResponse.json(users); } // POST /api/users export async function POST(request: Request) { const body = await request.json(); const user = await db.user.create({ data: body }); return NextResponse.json(user, { status: 201 }); }

5. 路由组 (Route Groups)

圆括号(groupName)创建的文件夹,不影响 URL,仅用于组织代码。

5.1 按布局分组

app/ ├── (auth)/ │ ├── layout.tsx ← 登录/注册专用布局(无导航栏) │ ├── login/ │ │ └── page.tsx → /login │ └── register/ │ └── page.tsx → /register ├── (marketing)/ │ ├── layout.tsx ← 营销页面布局(有导航栏 + 页脚) │ ├── page.tsx → / │ └── about/ │ └── page.tsx → /about
  • /login/register共享(auth)布局(无导航栏)。
  • //about共享(marketing)布局(有导航栏)。
  • URL 中不包含(auth)(marketing)

5.2 按团队/功能分组

app/ ├── (admin)/ │ ├── dashboard/ │ │ └── page.tsx → /dashboard │ └── users/ │ └── page.tsx → /users └── (store)/ ├── products/ │ └── page.tsx → /products └── cart/ └── page.tsx → /cart

6. 平行路由 (Parallel Routes)

@slotName命名的文件夹,允许在同一布局中同时渲染多个页面

6.1 结构

app/ ├── layout.tsx ← 引用 @analytics 和 @team ├── @analytics/ │ └── page.tsx ← 分析面板 ├── @team/ │ └── page.tsx ← 团队面板 └── page.tsx ← 主内容

6.2 布局接收 slots

// app/layout.tsx export default function Layout({ children, analytics, team, }: { children: React.ReactNode; analytics: React.ReactNode; team: React.ReactNode; }) { return ( <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr' }}> <div>{analytics}</div> <div>{children}</div> <div>{team}</div> </div> ); }

6.3 典型场景:模态框

app/ ├── layout.tsx ├── @modal/ │ ├── (.)login/ ← 拦截 /login,在模态框中显示 │ │ └── page.tsx │ └── default.tsx ← 模态框未激活时显示 null ├── login/ │ └── page.tsx ← 直接访问 /login 的完整页面 └── page.tsx
// app/@modal/default.tsx export default function Default() { return null; // 模态框未激活时不显示 } // app/layout.tsx export default function Layout({ children, modal }) { return ( <> {children} {modal} </> ); }

7. 拦截路由 (Intercepting Routes)

在不改变 URL 的情况下,用另一个路由的内容渲染当前页面。

7.1 命名约定

约定含义
(.)folder拦截同级的 folder 路由
(..)folder拦截上一级的 folder 路由
(..)(..)folder拦截上两级的 folder 路由
(...)folder拦截根目录的 folder 路由

7.2 典型场景:照片弹窗

app/ ├── layout.tsx ├── @modal/ │ └── (.)photo/ │ └── [id]/ │ └── page.tsx ← 弹窗版本(从照片列表点击进入) ├── photo/ │ └── [id]/ │ └── page.tsx ← 完整页面版本(直接访问 URL) └── page.tsx ← 照片列表
  • 从列表点击:URL 变为/photo/123,但显示弹窗(拦截路由生效)。
  • 直接访问/photo/123:显示完整页面(拦截路由不生效)。
  • 刷新/photo/123:显示完整页面(刷新不走客户端导航)。

8. 页面导航

8.1<Link>组件

import Link from 'next/link'; export default function Nav() { return ( <nav> <Link href="/">首页</Link> <Link href="/about">关于</Link> <Link href="/blog/hello-world">文章</Link> {/* 动态链接 */} <Link href={`/blog/${post.slug}`}>{post.title}</Link> {/* 激活状态 */} <Link href="/about" scroll={false}>关于(不滚动到顶部)</Link> </nav> ); }

特性

  • 视口内的<Link>自动预取(prefetch)目标页面。
  • 客户端导航,不会整页刷新
  • 自动滚动到页面顶部(可关闭)。

8.2 编程式导航

'use client'; import { useRouter } from 'next/navigation'; export default function LoginForm() { const router = useRouter(); async function handleSubmit() { const success = await login(); if (success) { router.push('/dashboard'); // 导航到 /dashboard router.refresh(); // 刷新当前路由(重新获取数据) } } return <button onClick={handleSubmit}>登录</button>; }
方法作用
router.push(href)导航到新页面(加入历史记录)
router.replace(href)替换当前页面(不加入历史记录)
router.back()返回上一页
router.refresh()刷新当前路由,重新获取服务端数据

8.3 导航行为对比

行为<Link>router.push<a>标签
客户端导航(整页刷新)
预取
历史记录加入加入加入
适用场景大多数导航表单提交后跳转外部链接

9. 路由元数据 (Metadata)

9.1 静态元数据

// app/about/page.tsx export const metadata = { title: '关于我们', description: '这是关于我们的页面', }; export default function AboutPage() { return <h1>关于我们</h1>; }

9.2 动态元数据

// app/blog/[slug]/page.tsx export async function generateMetadata({ params }) { const post = await getPost(params.slug); return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: [post.coverImage], }, }; }

9.3 布局中的元数据继承

子页面的title覆盖父布局的title,也可用模板语法:

// app/layout.tsx export const metadata = { title: { default: '我的网站', // 默认标题 template: '%s | 我的网站', // 子页面标题模板 }, }; // app/about/page.tsx export const metadata = { title: '关于我们', // 最终: "关于我们 | 我的网站" };

10. 完整路由速查表

文件/文件夹URL说明
app/page.tsx/首页
app/about/page.tsx/about静态路由
app/blog/[slug]/page.tsx/blog/hello动态路由
app/shop/[...slug]/page.tsx/shop/a/b/c捕获所有路由
app/docs/[[...slug]]/page.tsx/docs/docs/a/b可选捕获所有路由
app/(auth)/login/page.tsx/login路由组(不影响 URL)
app/@modal/(.)photo/[id]/page.tsx/photo/123平行路由 + 拦截路由
app/api/users/route.ts/api/usersAPI 路由

总结

Next.js 路由系统的核心记忆点:

  1. 文件即路由page.tsx定义可访问路径,文件夹名即 URL 段
  2. 动态参数[slug]单段、[...slug]多段、[[...slug]]可选多段
  3. 布局嵌套layout.tsx从根到叶逐层嵌套,导航时不销毁
  4. 路由组(group)组织代码不影响 URL
  5. 平行路由@slot同一布局渲染多个独立视图
  6. 拦截路由(.)在客户端导航时拦截,直接访问不拦截
  7. 特殊文件loading/error/not-found处理非理想状态
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/11 2:49:45

揭秘AI工具系统提示词:从逆向工程到定制化实践

1. 项目概述&#xff1a;一个AI工具系统提示词的“开源情报库”如果你和我一样&#xff0c;每天都在和各种AI工具打交道——从Cursor、Windsurf这样的AI编程IDE&#xff0c;到GitHub Copilot、Perplexity这类辅助工具——那你肯定不止一次好奇过&#xff1a;它们内部到底是怎么…

作者头像 李华
网站建设 2026/5/11 2:49:45

基于MCP协议实现AI开发上下文永续:claude-code-bridge项目实战

1. 项目概述&#xff1a;一个为AI开发者设计的上下文永续工具 如果你和我一样&#xff0c;日常开发流程已经深度嵌入了像Claude这样的AI助手&#xff0c;那你肯定对“上下文丢失”这个痛点深有体会。我经常在Claude的桌面应用里花上二三十分钟&#xff0c;和它一起头脑风暴&am…

作者头像 李华
网站建设 2026/5/11 2:47:33

手把手教你为STM32的SD卡驱动FatFs:从AU Size到disk_ioctl的完整配置流程

STM32实战&#xff1a;从SD卡协议到FatFs移植的全流程解析 在嵌入式开发中&#xff0c;存储系统设计往往是项目成败的关键一环。当我们需要在STM32平台上实现可靠的文件存储功能时&#xff0c;SD卡配合FatFs文件系统无疑是最经典的组合方案之一。然而&#xff0c;从硬件接口调试…

作者头像 李华
网站建设 2026/5/11 2:44:34

深入了解场效应管(FET)的基本原理与特性分析

场效应管&#xff08;FET&#xff09;基础概念场效应管&#xff08;Field Effect Transistor, FET&#xff09;是一种通过电场效应控制电流的半导体器件&#xff0c;属于电压控制型器件。其核心特点包括高输入阻抗、低驱动功耗和单极型载流子传导&#xff08;仅多数载流子参与导…

作者头像 李华
网站建设 2026/5/11 2:40:03

基于Godot引擎的模块化RTS游戏框架开发实战指南

1. 项目概述&#xff1a;当开放世界RTS遇上Godot引擎如果你和我一样&#xff0c;是个对即时战略游戏&#xff08;RTS&#xff09;有情怀&#xff0c;同时又对Godot引擎的轻量与高效念念不忘的开发者&#xff0c;那么看到“lampe-games/godot-open-rts”这个项目标题时&#xff…

作者头像 李华