前言
大家好,我是鲫小鱼。是一名不写前端代码的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!
第四章:Next.js App Router 核心概念与实践
教程简介
欢迎来到 Next.js 演进中最激动人心的部分——App Router。作为 Next.js 13+ 的核心特性,App Router 建立在 React Server Components (RSC) 的基础上,彻底改变了我们构建全栈应用的方式。它不仅提供了更灵活、更强大的路由和布局系统,还通过默认的服务端组件范式,带来了显著的性能提升。
在本章中,我们将深入探讨 App Router 的引入目的、与 Pages Router 的核心差异,并详细解析其基于目录的约定,特别是 layout.tsx, page.tsx, loading.tsx, error.tsx等特殊文件的作用。我们还将结合企业级开发、移动端适配、SEO、权限控制、全栈开发等实际场景,分享最佳实践和常见问题解决方案。通过本章的学习,您将掌握 App Router 的核心思想,并能够利用它来构建现代化、高性能的 Next.js 应用。
理论讲解
1.1 App Router 的引入目的与 Pages Router 对比
App Router 的诞生是为了解决 Pages Router 在复杂应用场景下面临的一些挑战,并充分利用 React 最新的特性(如 Server Components 和 Suspense)。
核心差异对比:
| 特性 | Pages Router (pages/) | App Router (app/) |
|---|---|---|
| 基础范式 | 客户端组件 (Client Components) 为主 | 服务端组件 (Server Components) 为主,按需使用客户端组件 |
| 路由定义 | 基于文件,每个文件是一个路由 | 基于目录,每个目录是一个路由段,page.tsx 作为该路由的 UI |
| 布局 | 通过 _app.tsx 和自定义组件实现,较为受限 | 内置层级布局系统 (layout.tsx),支持嵌套布局和状态保持 |
| 数据获取 | getServerSideProps, getStaticProps 等专用 API | 直接在服务端组件中使用 async/await,与 fetch API 深度集成,更灵活 |
| 加载/错误 UI | 需要手动实现 | 内置 loading.tsx 和 error.tsx,利用 React Suspense 和 Error Boundaries 自动处理 |
| 全栈能力 | API Routes (pages/api) | API Routes 依然支持,并新增 Server Actions 用于在组件中直接调用服务端逻辑 |
| SEO | 需手动配置 meta、head | generateMetadata、<Head /> 组件,支持分层 SEO、Open Graph、移动端适配 |
| 权限控制 | 需手动在页面或 _app.tsx 处理 | 可在 layout、middleware、Server Actions 等多层灵活处理 |
1.2 目录约定与最佳实践
App Router 通过一系列特殊文件来定义路由的行为和 UI。
page.tsx: 定义路由的唯一 UI。例如,app/dashboard/page.tsx会渲染/dashboard路径。layout.tsx: 定义跨多个页面共享的 UI。一个layout会包裹其子目录中的所有页面和子布局。布局是层级嵌套的,并且在导航时保持状态。loading.tsx: 一个加载状态 UI,它会利用 React Suspense 自动包裹page.tsx。当页面内容正在加载时,Next.js 会自动显示loading.tsx的内容,提供即时的加载反馈。error.tsx: 一个错误处理 UI,它会利用 React Error Boundary 自动包裹page.tsx。当页面或其子组件抛出错误时,会显示error.tsx的内容,并提供一个函数来尝试重新渲染。not-found.tsx: 当notFound()函数被调用或 URL 不匹配任何路由时,会显示此 UI。template.tsx: 与layout.tsx类似,但它在每次导航时都会创建一个新的实例,状态不会被保留。适用于需要每次进入都执行useEffect或重新获取数据的场景。
最佳实践:
- 充分利用嵌套布局,将全局导航、侧边栏、用户信息等放在不同层级的 layout 中,提升复用性和性能。
- 使用
loading.tsx提升用户体验,避免页面白屏。 - 在
error.tsx中集成错误上报(如 Sentry),并为用户提供重试按钮。 - 合理拆分服务端组件和客户端组件,减少 bundle 体积。
- 使用
generateMetadata动态生成 SEO 信息,支持多语言和移动端适配。
1.3 服务端组件 (RSC) 与客户端组件的协作
App Router 最大的创新在于 Server Components。服务端组件可以直接访问数据库、API、文件系统等后端资源,极大提升了数据获取的灵活性和安全性。
常见协作模式:
- 服务端组件负责数据获取和页面骨架渲染,客户端组件负责交互和动画。
- 通过 props 将服务端获取的数据传递给客户端组件。
- 客户端组件可通过 Server Actions 直接调用服务端逻辑,实现无 API 层的全栈开发体验。
注意事项:
- 客户端组件不能直接访问服务端资源。
- 服务端组件不能使用 React 的浏览器 Hooks(如 useState、useEffect)。
- 组件树中只要有一个客户端组件,其所有子组件都将被打包到客户端。
1.4 路由定义、嵌套布局与分组
App Router 支持路由分组(Group)、并行路由(Parallel Routes)、拦截路由(Intercepting Routes)等高级特性。
- 路由分组:通过
(group)目录实现 URL 不变但结构分组,便于代码组织。 - 并行路由:通过
@slot目录实现同一页面多个区域独立渲染,适合多标签页、弹窗等场景。 - 拦截路由:通过
(..)(route)目录实现弹窗、侧边栏等 UI 层级的路由切换。
示例结构:
app/
dashboard/
layout.tsx
page.tsx
(settings)/
page.tsx
(profile)/
page.tsx
(auth)/
login/page.tsx
register/page.tsx
1.5 SEO、移动端适配与性能优化
-
SEO:利用
generateMetadata、<Head />组件为每个页面动态生成 title、description、Open Graph、Twitter Card 等 meta 标签。 -
移动端适配:在
layout.tsx的<head>中添加 viewport、theme-color 等 meta 标签,结合 Tailwind CSS 或 CSS Modules 实现响应式布局。 -
性能优化:
- 利用服务端组件减少客户端 bundle 体积。
- 使用 Suspense 和
loading.tsx优化加载体验。 - 利用
next/image、next/font进行图片和字体优化。 - 合理拆分并行路由,提升页面响应速度。
1.6 权限控制与企业级场景
-
权限控制:可在 layout、middleware、Server Actions 等多层实现。
- 在 layout.tsx 中判断用户权限,未登录时重定向到登录页。
- 在 Server Actions 中校验用户身份,防止未授权操作。
- 使用中间件(
middleware.ts)实现全局路由守卫。
-
企业级场景:
- 多租户系统:通过 layout 分层实现租户隔离。
- 大型仪表盘:利用并行路由和嵌套布局实现复杂 UI。
- 国际化:结合
generateMetadata和多语言路由,支持多语言 SEO。
1.7 App Router 与全栈开发
- API Routes:
app/api/目录下可继续使用 API Routes,处理 RESTful 或 GraphQL 请求。 - Server Actions:在服务端组件或客户端组件中直接定义
async function,通过表单或按钮触发,简化数据变更和表单提交。 - 数据库集成:服务端组件可直接访问数据库(如 Prisma、Drizzle、TypeORM),无需额外 API 层。
- 文件上传:通过 Server Actions 或 API Routes 实现安全的文件上传。
1.8 常见问题与解决方案
- 页面闪烁/白屏:确保
loading.tsx存在并优化骨架屏。 - 客户端组件 bundle 过大:合理拆分组件,避免服务端组件中嵌套过多客户端组件。
- SEO 不生效:检查
generateMetadata是否正确实现,确保 meta 标签动态生成。 - 权限绕过:所有敏感操作务必在服务端校验权限。
- 移动端适配问题:使用响应式 CSS,测试主流设备兼容性。
代码示例
2.1 创建一个带有全局布局的仪表盘应用
我们将创建一个 /dashboard 路由,它有自己的布局,独立于应用的根布局。
-
创建根布局 (
app/layout.tsx) :// app/layout.tsx import type { Metadata } from 'next'; import './globals.css'; // 假设有全局样式 export const metadata: Metadata = { title: 'Next.js 教程应用', description: '由鲫小鱼创建', }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <header style={{ padding: '1rem', borderBottom: '1px solid #ccc' }}> <h1>我的应用</h1> </header> <main>{children}</main> </body> </html> ); } -
创建仪表盘布局 (
app/dashboard/layout.tsx) :// app/dashboard/layout.tsx import Link from 'next/link'; export default function DashboardLayout({ children }: { children: React.ReactNode }) { return ( <section style={{ display: 'flex' }}> <nav style={{ width: '200px', borderRight: '1px solid #ccc', padding: '1rem' }}> <h2>仪表盘</h2> <ul> <li><Link href="/dashboard">主页</Link></li> <li><Link href="/dashboard/settings">设置</Link></li> </ul> </nav> <div style={{ flex: 1, padding: '1rem' }}> {children} </div> </section> ); } -
创建仪表盘页面 (
app/dashboard/page.tsx) :// app/dashboard/page.tsx export default function DashboardPage() { return <div>欢迎来到仪表盘!</div>; } -
创建设置页面 (
app/dashboard/settings/page.tsx) :// app/dashboard/settings/page.tsx export default function SettingsPage() { return <div>这里是设置页面。</div>; }
现在访问 /dashboard 或 /dashboard/settings,你会看到它们都被仪表盘的侧边栏布局包裹,并且整个应用又有全局的 RootLayout。
2.2 演示 loading.js 和 error.js 的使用
-
创建加载状态 UI (
app/dashboard/loading.tsx) :// app/dashboard/loading.tsx export default function Loading() { // 你可以使用任何 UI 库的骨架屏 return <h2>🌀 加载中...</h2>; } -
在设置页面模拟数据加载延迟:
// app/dashboard/settings/page.tsx (修改后) // 模拟数据获取 async function getData() { await new Promise((resolve) => setTimeout(resolve, 2000)); // 模拟2秒延迟 return { theme: 'dark' }; } export default async function SettingsPage() { const data = await getData(); return ( <div> <h1>设置页面</h1> <p>当前主题: {data.theme}</p> </div> ); }现在,当您从
/dashboard导航到/dashboard/settings时,会立即看到 "🌀 加载中..." 的提示,2秒后才会显示页面内容。 -
创建错误处理 UI (
app/dashboard/error.tsx) :// app/dashboard/error.tsx 'use client'; // 错误组件必须是客户端组件 import { useEffect } from 'react'; export default function Error({ error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { useEffect(() => { // 可以将错误上报给日志服务 console.error(error); }, [error]); return ( <div> <h2>出错了!</h2> <button onClick={() => reset()}>再试一次</button> </div> ); } -
在页面中故意抛出错误:
// app/dashboard/settings/page.tsx (再次修改) export default async function SettingsPage() { // 故意抛出错误以测试 error.tsx if (Math.random() > 0.5) { throw new Error('加载设置失败!'); } return <div>设置页面</div>; }现在刷新
/dashboard/settings页面,有 50% 的几率会看到 "出错了!" 的界面,并且可以通过点击按钮尝试重新渲染。
实战项目
3.1 将现有 Pages Router 的一部分功能迁移到 App Router
目标:体验新旧路由的差异与优势,将一个模拟的用户列表功能从 Pages Router 的思维模式转变为 App Router 的思维模式。
-
确定迁移功能:我们迁移一个用户列表页面
/users。 -
在
app目录下创建新路由:- 创建
app/users/page.tsx。 - (可选) 创建
app/users/layout.tsx如果需要特定布局。
- 创建
-
数据获取方式的转变:
-
旧方式 (Pages Router) :你可能会在
pages/users.tsx中使用getStaticProps或getServerSideProps。// pages/users.tsx (旧方式) // export async function getStaticProps() { ... } -
新方式 (App Router) :在
app/users/page.tsx(一个服务端组件) 中直接fetch数据。
-
-
实现
app/users/page.tsx:// app/users/page.tsx interface User { id: number; name: string; email: string; } // 在服务端组件中直接获取数据 async function getUsers(): Promise<User[]> { // 在真实应用中,这里会是 fetch('https://api.example.com/users') // 我们用一个模拟的 API const res = await fetch('https://jsonplaceholder.typicode.com/users'); if (!res.ok) { throw new Error('获取用户数据失败'); } return res.json(); } export default async function UsersPage() { const users = await getUsers(); return ( <div> <h1>用户列表</h1> <ul> {users.map((user) => ( <li key={user.id}> {user.name} ({user.email}) </li> ))} </ul> </div> ); } -
添加
loading.tsx和error.tsx:- 在
app/users/目录下创建loading.tsx和error.tsx,以提供更好的用户体验。
// app/users/loading.tsx export default function Loading() { return <p>正在加载用户列表...</p>; }// app/users/error.tsx 'use client'; export default function Error({ reset }: { reset: () => void }) { return ( <div> <p>加载用户列表失败!</p> <button onClick={() => reset()}>重试</button> </div> ); } - 在
体验差异与优势:
- 代码简洁:数据获取逻辑与 UI 更紧密地结合在一起,无需导出专门的函数。
- 默认服务端:默认就是高性能的服务端组件,无需额外配置。
- 内置 UX 优化:
loading和errorUI 的处理变得非常简单和自动化。 - 组件化更彻底:你可以将数据获取逻辑和 UI 封装在一个组件内,而不是分离到页面级函数中。
通过本章的学习,您已经掌握了 Next.js App Router 的核心概念,包括其与 Pages Router 的区别、基于目录的特殊文件约定、服务端与客户端组件的范式,以及如何利用嵌套布局和内置的加载/错误处理机制来构建现代化的 Web 应用。下一章,我们将深入探讨组件化的设计模式和高级样式管理。
最后感谢阅读!欢迎关注我,微信公众号:
《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!