Next.js 教程系列(四)Next.js App Router 核心概念与实践

440 阅读10分钟

前言

大家好,我是鲫小鱼。是一名不写前端代码的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!

第四章:Next.js App Router 核心概念与实践

教程简介

欢迎来到 Next.js 演进中最激动人心的部分——App Router。作为 Next.js 13+ 的核心特性,App Router 建立在 React Server Components (RSC) 的基础上,彻底改变了我们构建全栈应用的方式。它不仅提供了更灵活、更强大的路由和布局系统,还通过默认的服务端组件范式,带来了显著的性能提升。

在本章中,我们将深入探讨 App Router 的引入目的、与 Pages Router 的核心差异,并详细解析其基于目录的约定,特别是 layout.tsxpage.tsxloading.tsxerror.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),支持嵌套布局和状态保持
数据获取getServerSidePropsgetStaticProps 等专用 API直接在服务端组件中使用 async/await,与 fetch API 深度集成,更灵活
加载/错误 UI需要手动实现内置 loading.tsx 和 error.tsx,利用 React Suspense 和 Error Boundaries 自动处理
全栈能力API Routes (pages/api)API Routes 依然支持,并新增 Server Actions 用于在组件中直接调用服务端逻辑
SEO需手动配置 meta、headgenerateMetadata<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/imagenext/font 进行图片和字体优化。
    • 合理拆分并行路由,提升页面响应速度。

1.6 权限控制与企业级场景

  • 权限控制:可在 layout、middleware、Server Actions 等多层实现。

    • 在 layout.tsx 中判断用户权限,未登录时重定向到登录页。
    • 在 Server Actions 中校验用户身份,防止未授权操作。
    • 使用中间件(middleware.ts)实现全局路由守卫。
  • 企业级场景

    • 多租户系统:通过 layout 分层实现租户隔离。
    • 大型仪表盘:利用并行路由和嵌套布局实现复杂 UI。
    • 国际化:结合 generateMetadata 和多语言路由,支持多语言 SEO。

1.7 App Router 与全栈开发

  • API Routesapp/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 路由,它有自己的布局,独立于应用的根布局。

  1. 创建根布局 (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>
      );
    }
    
  2. 创建仪表盘布局 (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>
      );
    }
    
  3. 创建仪表盘页面 (app/dashboard/page.tsx)

    // app/dashboard/page.tsx
    export default function DashboardPage() {
      return <div>欢迎来到仪表盘!</div>;
    }
    
  4. 创建设置页面 (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 的使用

  1. 创建加载状态 UI (app/dashboard/loading.tsx)

    // app/dashboard/loading.tsx
    export default function Loading() {
      // 你可以使用任何 UI 库的骨架屏
      return <h2>🌀 加载中...</h2>;
    }
    
  2. 在设置页面模拟数据加载延迟

    // 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秒后才会显示页面内容。

  3. 创建错误处理 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>
      );
    }
    
  4. 在页面中故意抛出错误

    // 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 的思维模式。

  1. 确定迁移功能:我们迁移一个用户列表页面 /users

  2. 在 app 目录下创建新路由

    • 创建 app/users/page.tsx
    • (可选) 创建 app/users/layout.tsx 如果需要特定布局。
  3. 数据获取方式的转变

    • 旧方式 (Pages Router) :你可能会在 pages/users.tsx 中使用 getStaticProps 或 getServerSideProps

      // pages/users.tsx (旧方式)
      // export async function getStaticProps() { ... }
      
    • 新方式 (App Router) :在 app/users/page.tsx (一个服务端组件) 中直接 fetch 数据。

  4. 实现 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>
      );
    }
    
  5. 添加 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 和 error UI 的处理变得非常简单和自动化。
  • 组件化更彻底:你可以将数据获取逻辑和 UI 封装在一个组件内,而不是分离到页面级函数中。

通过本章的学习,您已经掌握了 Next.js App Router 的核心概念,包括其与 Pages Router 的区别、基于目录的特殊文件约定、服务端与客户端组件的范式,以及如何利用嵌套布局和内置的加载/错误处理机制来构建现代化的 Web 应用。下一章,我们将深入探讨组件化的设计模式和高级样式管理。

最后感谢阅读!欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!