Next.js项目结构解析:理解 App Router 架构(二)

📚 概述与目标

Next.js 提供了两种路由解决方案:传统的 Pages Router 和自 Next.js 13 起引入的 App Router。App Router 是基于 React Server Components (RSC) 构建的,代表了 Next.js 架构上的一次重大飞跃,旨在提供更优的性能、更灵活的数据获取方式以及更直观的路由管理。自 v13.4 起,App Router 已成为默认推荐的路由方案

对于希望掌握 Next.js 现代开发范式的开发者而言,深入理解 App Router 的项目结构和核心概念至关重要。本文将详细解析 Next.js App Router 的项目结构,介绍其核心概念、特殊文件约定以及它们在构建高性能全栈应用中的作用。通过本文的学习,你将能够:

  • 理解 Next.js 路由的演进,特别是 Pages Router 到 App Router 的转变。
  • 掌握 App Router 的基本原理和核心优势。
  • 熟悉 app/ 目录下的文件约定及其功能,包括 page.js, layout.js, loading.js, error.js, not-found.js, template.jsdefault.js
  • 区分并合理运用服务器组件与客户端组件。
  • 了解 App Router 中高效的数据获取新范式。

🚀 App Router 核心概念

App Router 的设计理念是围绕着约定优于配置服务器优先的原则。它将路由、数据获取和渲染逻辑紧密结合,提供了一种全新的开发体验。

1. 基于文件系统的路由与路由组

App Router 沿袭了 Next.js 约定优于配置的原则,通过文件系统来定义路由。在 app/ 目录下创建文件夹即可定义路由段(route segment),而 page.js 文件则用于定义该路由段的 UI。例如,app/dashboard/page.js 会映射到 /dashboard 路径。

Next.js App Router 还引入了 "路由组"(Route Groups) 的概念,允许你将路由段组织到逻辑组中,而不会影响 URL 路径。这对于组织复杂的路由结构、创建不同的布局或管理多个根布局非常有用。路由组通过将文件夹名包裹在括号中来定义,例如 (marketing)(shop)

jsx 复制代码
// app/dashboard/page.js
export default function DashboardPage() {
  return <h1>Dashboard</h1>;
}

// app/(marketing)/about/page.js
// 映射到 /about 路径,但属于 marketing 路由组
export default function AboutPage() {
  return <h1>About Us</h1>;
}

2. 服务器组件与客户端组件

Next.js App Router 的核心创新是引入了 React 服务器组件(Server Components),它改变了传统的 React 应用渲染模式。在 app/ 目录下,所有组件默认都是服务器组件,而客户端组件则需要通过 'use client' 指令明确声明。

服务器组件 (Server Components)

服务器组件在服务器端渲染,并将渲染结果(HTML/CSS)发送到客户端。它们不包含客户端 JavaScript,因此可以显著减少客户端的 JavaScript 包大小,从而提高初始页面加载性能和用户体验。

特点:

  • 渲染位置:在服务器上渲染。
  • 客户端 JavaScript:不发送到客户端,减少了客户端包大小。
  • 数据获取:可以直接访问后端资源(如数据库、文件系统、内部 API),无需额外的 API 层。
  • 安全性:敏感数据和逻辑保留在服务器端,不会暴露给客户端。
  • 性能优势:减少客户端渲染工作,加快页面加载速度,有利于 SEO。

适用场景:

  • 渲染静态或数据驱动的内容,如博客文章、产品列表、用户个人资料等。
  • 需要直接访问后端资源(数据库查询、文件读取)的组件。
  • 对 SEO 和初始加载性能要求较高的页面或组件。

客户端组件 (Client Components)

客户端组件在浏览器中渲染,并允许你使用 React Hooks(如 useState, useEffect)和浏览器特有的 API(如 localStorage, window)。它们需要将 JavaScript 发送到客户端进行 "hydration"(水合),使其具备交互能力。

jsx 复制代码
// app/components/Counter.js
'use client'; // 明确声明为客户端组件

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

特点:

  • 渲染位置:在客户端(浏览器)上渲染。
  • 客户端 JavaScript:需要发送到客户端进行水合,以实现交互。
  • 交互性:支持事件处理、状态管理、生命周期 Hooks 等客户端交互功能。
  • 浏览器 API 访问 :可以访问 window, document 等浏览器特有对象。

适用场景:

  • 需要用户交互的组件,如表单、按钮、轮播图、交互式图表等。
  • 依赖 React Hooks(如 useState, useEffect, useContext)的组件。
  • 需要访问浏览器特有 API 的组件。

两者协同工作

在 App Router 中,服务器组件和客户端组件可以无缝地协同工作。你可以在服务器组件中导入并使用客户端组件,Next.js 会自动处理它们的渲染和水合过程。这种混合渲染模式使得开发者能够根据组件的具体需求选择最佳的渲染策略,从而构建出高性能且高度交互的应用程序。

3. 约定式特殊文件

App Router 引入了一系列具有特殊含义的文件名,它们定义了路由段的特定行为和 UI 结构。这些文件都必须放置在路由段的文件夹内。

文件名 作用 渲染位置 示例用途
page.js 定义路由段的唯一 UI,必须包含一个默认导出 React 组件。 服务器/客户端 /dashboard/page.js 定义 /dashboard 路径的页面内容。
layout.js 定义路由段及其子路由的共享 UI 布局,必须接收 children prop。 服务器 app/layout.js 定义全局布局;dashboard/layout.js 定义仪表盘布局。
loading.js 定义路由段加载时的 UI,在数据获取或渲染进行时显示。 服务器 在数据加载时显示骨架屏或加载动画。
error.js 定义路由段的错误边界 UI,捕获并显示子组件树中的错误。 客户端 显示友好的错误信息,并提供重试按钮。
not-found.js 定义路由段的 404 页面 UI,当路由未匹配时显示。 服务器 自定义 404 页面。
template.js 类似于 layout.js,但每次导航时都会重新渲染其子组件。 服务器 需要在每次导航时重置状态的场景(如动画)。
default.js 用于平行路由,作为未匹配到活跃路由时的默认 UI。 服务器/客户端 在复杂仪表盘中,当某个面板未激活时显示默认内容。

图片:App Router 特殊文件结构示意图

4. 数据获取

Next.js App Router 在数据获取方面引入了革命性的变化,充分利用了服务器组件的优势,使得数据获取更加高效和灵活。

服务器端数据获取

在 App Router 中,你可以在服务器组件中直接进行数据获取,而无需创建单独的 API 路由。这意味着你可以直接在组件内部进行数据库查询、文件系统读取或调用内部服务,极大地简化了数据流,减少了客户端和服务器之间的往返,提高了性能。

特点:

  • 直接访问后端:服务器组件可以直接与数据库、文件系统或其他后端服务交互。
  • 简化数据流:无需单独的 API 层,数据直接从服务器传递到组件。
  • 自动缓存与去重 :Next.js 会自动缓存 fetch 请求,并对相同的请求进行去重,优化性能。
  • 灵活的缓存策略 :通过 fetchcache 选项(如 no-store, force-cache)和 revalidate 选项,可以精细控制数据的缓存行为和重新验证策略。
jsx 复制代码
// app/dashboard/page.js (Server Component)
async function getPosts() {
  // 直接在服务器组件中获取数据,可以访问数据库或内部API
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // 每小时重新验证一次数据
  });
  if (!res.ok) {
    // 推荐使用 Next.js 提供的 notFound() 或 throw new Error() 处理错误
    throw new Error('Failed to fetch posts');
  }
  return res.json();
}

export default async function DashboardPage() {
  const posts = await getPosts();
  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

客户端数据获取

尽管服务器端数据获取是 App Router 的推荐方式,但在某些需要实时交互或用户特定数据的场景下,客户端数据获取仍然是必要的。你可以在客户端组件中使用传统的 客户端数据获取库(如 SWR, React Query )或浏览器原生的 fetch API 来获取数据。通常,这些客户端请求会指向 Next.js 提供的 API 路由(Route Handlers)或外部 API。

jsx 复制代码
// app/components/ClientDataFetcher.js (Client Component)
'use client';

import { useEffect, useState } from 'react';

export default function ClientDataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchData() {
      try {
        // 在客户端组件中通过 API 路由获取数据
        const res = await fetch('/api/some-client-data');
        const result = await res.json();
        setData(result);
      } catch (error) {
        console.error('Error fetching client data:', error);
      } finally {
        setLoading(false);
      }
    }
    fetchData();
  }, []);

  if (loading) return <p>Loading client data...</p>;
  if (!data) return <p>No client data.</p>;

  return (
    <div>
      <h2>Client Data</h2>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

总结: App Router 提供了强大的混合数据获取能力,鼓励在服务器组件中进行数据获取以优化性能和简化逻辑,同时保留了客户端数据获取的灵活性,以满足各种应用场景的需求。

📂 项目结构概览

一个典型的 Next.js App Router 项目结构如下:

python 复制代码
my-next-app/
├── app/                  # App Router 核心目录
│   ├── (group)/          # 路由组 (可选)
│   │   ├── layout.js
│   │   └── page.js
│   ├── api/              # Route Handlers (API 路由)
│   │   └── route.js
│   ├── dashboard/
│   │   ├── layout.js     # 仪表盘布局
│   │   ├── page.js       # 仪表盘主页
│   │   ├── settings/
│   │   │   └── page.js   # /dashboard/settings 页面
│   │   ├── loading.js    # 仪表盘加载状态
│   │   └── error.js      # 仪表盘错误边界
│   ├── blog/
│   │   ├── [slug]/
│   │   │   └── page.js   # 动态路由,如 /blog/my-first-post
│   │   └── page.js       # /blog 页面
│   ├── layout.js         # 根布局 (必需)
│   └── page.js           # 根页面 (必需)
├── public/               # 静态资源目录
│   └── favicon.ico
├── components/           # 可复用组件 (非路由相关)
│   ├── Button.js
│   └── Navbar.js
├── lib/                  # 辅助函数、工具类、数据库连接等
│   ├── utils.js
│   └── db.js
├── styles/               # 全局样式或 CSS 变量
│   └── globals.css
├── next.config.js        # Next.js 配置文件
├── package.json          # 项目依赖与脚本
├── tsconfig.json         # TypeScript 配置文件 (如果使用 TypeScript)
├── .env                  # 环境变量文件
└── .gitignore            # Git 忽略文件

深入解析 app/ 目录下的特殊文件

1. layout.js (布局)

layout.js 文件定义了 UI 共享的布局,它会包裹其子路由或子页面。布局在导航时会保持状态,不会重新渲染,这对于保持交互状态和提升用户体验非常重要。每个 app/ 目录下都可以有一个 layout.js 文件,形成嵌套布局。

作用与特点:

  • 共享 UI 结构:布局组件包裹其子路由段,提供共享的 UI 结构,例如导航栏、页脚、侧边栏等。
  • 状态保持:在导航到同一布局内的不同页面时,布局组件会保持其内部状态,不会重新渲染,从而提供更流畅的用户体验。
  • 嵌套布局 :你可以在不同层级(文件夹)定义 layout.js 文件,它们会自动嵌套。例如,app/layout.js 是根布局,app/dashboard/layout.js 会嵌套在根布局内部,并应用于 /dashboard 及其子路由。
  • 必需的根布局app/layout.js 是整个 Next.js 应用的必需根布局,它必须包含 <html><body> 标签。
  • 服务器组件layout.js 默认是服务器组件,可以在其中直接进行数据获取。

与 Pages Router 的区别:

在 Pages Router 中,布局通常通过自定义 _app.js 或在每个页面中手动引入组件来实现,状态保持和嵌套管理相对复杂。App Router 的 layout.js 提供了一种更原生、更声明式的方式来管理布局,并自动处理状态保持和嵌套。

示例:

jsx 复制代码
// app/layout.js (根布局 - Root Layout)
// 必须包含 <html> 和 <body> 标签
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <header>全局头部</header>
        {children}
        <footer>全局底部</footer>
      </body>
    </html>
  );
}

// app/dashboard/layout.js (嵌套布局)
// 会嵌套在 RootLayout 内部,应用于 /dashboard 及其子路由
export default function DashboardLayout({ children }) {
  return (
    <section>
      <nav>Dashboard 导航</nav>
      {children}
      <div>Dashboard 侧边栏</div>
    </section>
  );
}

// app/dashboard/settings/page.js
// 这个页面会同时被 RootLayout 和 DashboardLayout 包裹
export default function DashboardSettingsPage() {
  return <h1>Dashboard Settings</h1>;
}

2. page.js (页面)

page.js 文件是 App Router 中定义页面 UI 的核心文件。每个路由段(文件夹)都必须包含一个 page.js 文件(或 route.js 用于 API 路由),它负责渲染该路由路径下的最终用户界面。

作用与特点:

  • 定义页面 UIpage.js 导出的 React 组件是用户访问特定 URL 路径时看到的内容。
  • 路由入口:它是路由段的入口点,Next.js 会根据文件系统结构自动将其映射到对应的 URL 路径。
  • 默认服务器组件page.js 默认是服务器组件,这意味着你可以在其中直接使用 async/await 进行数据获取,而无需额外的 API 层。
  • 嵌套在布局中page.js 组件会作为 children prop 传递给其父级 layout.js 组件,从而被父级布局包裹。

示例:

jsx 复制代码
// app/about/page.js
// 访问 /about 路径时会渲染此页面
export default function AboutPage() {
  return (
    <main>
      <h1>关于我们</h1>
      <p>这是一个关于我们的页面内容。</p>
    </main>
  );
}

// app/products/[slug]/page.js
// 动态路由示例,例如 /products/nextjs-book
export default async function ProductDetailPage({ params }) {
  const productId = params.slug;
  // 可以在服务器组件中直接获取产品数据
  const product = await getProductData(productId);

  if (!product) {
    // 可以使用 notFound() 函数触发 not-found.js
    // notFound();
    return <p>产品未找到。</p>;
  }

  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>价格: ${product.price}</p>
    </main>
  );
}

// 假设有一个获取产品数据的函数
async function getProductData(id) {
  // 模拟数据获取
  const products = {
    'nextjs-book': { name: 'Next.js 权威指南', description: '一本深入讲解 Next.js 的书籍', price: 59.99 },
    'react-course': { name: 'React 实战课程', description: '手把手教你开发 React 应用', price: 199.99 },
  };
  return new Promise(resolve => setTimeout(() => resolve(products[id]), 500));
}

3. loading.js (加载状态)

loading.js 文件允许你在页面内容或布局加载时显示一个即时的加载状态 UI。当路由段的数据或组件正在加载时,Next.js 会自动显示 loading.js 中定义的 UI,从而提供更好的用户体验。

作用与特点:

  • 即时加载反馈:在数据获取或组件渲染完成之前,向用户展示一个加载指示器,避免白屏。
  • 基于 React Suspenseloading.js 的实现原理是基于 React 的 Suspense API。当 page.jslayout.js 中有异步操作(如数据获取)时,Next.js 会自动将其包裹在一个 Suspense 边界内,并使用 loading.js 作为 fallback
  • 流式渲染 :结合 Suspense 和服务器组件,Next.js 可以实现流式渲染,即页面的不同部分可以独立加载和显示,而不是等待整个页面加载完成。
  • 层级匹配loading.js 会匹配其同级或下级的 page.jslayout.js。例如,app/dashboard/loading.js 会在 app/dashboard/page.jsapp/dashboard/layout.js 加载时显示。

示例:

jsx 复制代码
// app/dashboard/loading.js
// 当 /dashboard 页面或其数据正在加载时显示
export default function DashboardLoading() {
  return (
    <div style={{ padding: '20px', textAlign: 'center', fontSize: '24px' }}>
      <p>🚀 仪表盘数据加载中...</p>
      <img src="/loading.gif" alt="Loading..." style={{ width: '50px', height: '50px' }} />
    </div>
  );
}

// 假设 app/dashboard/page.js 中有异步数据获取
// app/dashboard/page.js
import { Suspense } from 'react';
import PostsList from './PostsList'; // 假设这是一个异步加载组件

export default function DashboardPage() {
  return (
    <main>
      <h1>我的仪表盘</h1>
      {/* PostsList 组件内部可能进行数据获取,会被 Suspense 捕获 */}
      <Suspense fallback={<p>加载文章列表...</p>}>
        <PostsList />
      </Suspense>
    </main>
  );
}

// app/dashboard/PostsList.js (假设这是一个异步组件)
async function getPosts() {
  const res = await fetch('https://api.example.com/posts');
  return res.json();
}

export default async function PostsList() {
  const posts = await getPosts();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

4. error.js (错误处理)

error.js 文件允许你为特定的路由段定义一个错误边界(Error Boundary),用于优雅地捕获其子路由段中的运行时错误,并显示一个回退 UI,而不是导致整个应用崩溃。它基于 React 的 Error Boundary 概念实现。

作用与特点:

  • 捕获运行时错误error.js 会捕获其同级或下级组件(包括 layout.js, page.js, template.js 以及它们内部的客户端组件和服务器组件)在渲染过程中发生的 JavaScript 错误。
  • 提供回退 UI :当错误发生时,error.js 中定义的 UI 会被渲染,取代出错的组件。
  • 必须是客户端组件 :Error Boundaries 必须是客户端组件,因此 error.js 文件顶部需要添加 'use client' 指令。
  • 提供恢复机制error.js 组件会接收 errorreset 两个 props。
    • error:表示捕获到的错误对象。
    • reset:一个函数,调用它可以尝试重新渲染错误边界内的内容,通常用于提供"重试"功能。
  • 错误日志 :你可以在 error.js 中使用 useEffect 钩子来记录错误到错误报告服务(如 Sentry, Bugsnag)。
  • 层级匹配error.js 会捕获其同级或下级路由段的错误。如果一个错误在 error.js 无法捕获的层级发生(例如在 layout.js 的父级),则会向上冒泡到最近的 error.jsglobal-error.js

示例:

jsx 复制代码
// app/dashboard/error.js
'use client'; // Error Boundaries 必须是客户端组件

import { useEffect } from 'react';

export default function Error({ error, reset }) {
  useEffect(() => {
    // 可以在这里将错误记录到错误报告服务
    console.error('捕获到错误:', error);
  }, [error]);

  return (
    <div style={{ padding: '20px', border: '1px solid red', borderRadius: '8px', textAlign: 'center' }}>
      <h2>出错了!</h2>
      <p>很抱歉,页面加载时发生了一个问题。</p>
      <button
        onClick={
          // 尝试通过重新渲染来恢复
          () => reset()
        }
        style={{ padding: '10px 20px', backgroundColor: '#0070f3', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
      >
        尝试重新加载
      </button>
    </div>
  );
}

global-error.js

为了捕获根布局(app/layout.js)中的错误,你需要创建一个 app/global-error.js 文件。这个文件会捕获所有未被其他 error.js 文件捕获的错误,包括根布局中的错误。global-error.js 必须包含自己的 <html><body> 标签,因为它会完全替换根布局。

jsx 复制代码
// app/global-error.js
'use client';

export default function GlobalError({ error, reset }) {
  return (
    <html>
      <body>
        <div style={{ padding: '50px', textAlign: 'center', backgroundColor: '#f8d7da', color: '#721c24' }}>
          <h1>全局错误!</h1>
          <p>应用程序发生了一个未预期的错误。</p>
          <button onClick={() => reset()}>重试</button>
        </div>
      </body>
    </html>
  );
}

5. not-found.js (未找到页面)

not-found.js 文件允许你为应用程序定义一个自定义的 404 Not Found 页面。当用户尝试访问一个不存在的路由时,或者在服务器组件中通过 notFound() 函数手动触发时,Next.js 会显示此文件定义的 UI。

作用与特点:

  • 自定义 404 页面:提供一个友好的用户界面,告知用户请求的资源未找到。
  • 自动触发 :当 Next.js 无法匹配到任何路由时,会自动渲染最近的 not-found.js 文件。
  • 手动触发 :你可以在服务器组件中导入并调用 notFound() 函数来手动触发 404 页面,这在数据未找到或权限不足等场景下非常有用。
  • 层级匹配 :与 layout.jserror.js 类似,not-found.js 也可以在不同层级定义,Next.js 会从当前路由段向上查找最近的 not-found.js

示例:

jsx 复制代码
// app/not-found.js (全局 404 页面)
import Link from 'next/link';

export default function NotFound() {
  return (
    <div style={{ textAlign: 'center', padding: '50px' }}>
      <h1>404 - 页面未找到</h1>
      <p>抱歉,您请求的页面不存在。</p>
      <Link href="/" style={{ color: '#0070f3', textDecoration: 'underline' }}>
        返回首页
      </Link>
    </div>
  );
}

// 假设在 app/products/[slug]/page.js 中手动触发 404
// app/products/[slug]/page.js
import { notFound } from 'next/navigation'; // 导入 notFound 函数

export default async function ProductDetailPage({ params }) {
  const productId = params.slug;
  const product = await getProductData(productId);

  if (!product) {
    // 如果产品不存在,手动触发 404 页面
    notFound(); 
  }

  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </main>
  );
}

// 假设有一个获取产品数据的函数
async function getProductData(id) {
  // 模拟数据获取,如果 id 为 'unknown' 则返回 null
  const products = {
    'nextjs-book': { name: 'Next.js 权威指南', description: '一本深入讲解 Next.js 的书籍', price: 59.99 },
  };
  return new Promise(resolve => setTimeout(() => resolve(products[id] || null), 300));
}

6. template.js (模板)

template.js 文件与 layout.js 类似,也用于包裹其子布局或页面。然而,它们之间存在一个关键区别:当用户在同一路由段内导航时,template.js 会重新渲染并创建新的实例,而 layout.js 则会保持状态,不会重新渲染。

作用与特点:

  • 强制重新渲染template.js 每次导航时都会创建一个新的实例,其内部的状态不会被保留。这使得它非常适合需要"重置"状态或执行进入/退出动画的场景。
  • 渲染层级template.js 位于 layout.jspage.js 之间,即 layout 包裹 templatetemplate 包裹 page
  • 适用于动画 :由于其重新渲染的特性,template.js 是实现页面切换动画的理想选择,例如使用 CSS 过渡或动画库。
  • 依赖 useEffectuseState 的功能 :如果你的页面或布局依赖于 useEffectuseState 来执行某些初始化操作或管理状态,并且希望在每次导航时都重新执行这些操作,那么 template.js 会非常有用。

layout.js 的区别:

  • 状态保持layout.js 保持状态,template.js 不保持状态(每次导航都重新挂载)。
  • 重新渲染layout.js 不重新渲染,template.js 重新渲染。

示例:

jsx 复制代码
// app/dashboard/template.js
'use client'; // 如果需要使用 Hooks 或浏览器 API,则声明为客户端组件

import { useEffect } from 'react';

export default function DashboardTemplate({ children }) {
  useEffect(() => {
    console.log('DashboardTemplate 重新挂载');
    // 可以在这里执行进入动画
  }, []);

  return (
    <div style={{ border: '2px dashed blue', padding: '20px' }}>
      <h2>Dashboard Template</h2>
      {children}
    </div>
  );
}

// 假设有一个页面 app/dashboard/settings/page.js
// 当从 /dashboard/profile 导航到 /dashboard/settings 时,DashboardTemplate 会重新挂载

7. default.js (默认文件)

default.js 文件主要用于并行路由(Parallel Routes)中,当一个路由槽(slot)没有被激活(例如,通过刷新页面或直接访问 URL,导致并行路由的某个部分没有被渲染)时,default.js 会提供一个默认的 UI 作为后备。

作用与特点:

  • 并行路由的后备 UI :在并行路由的场景下,如果某个路由槽的内容没有被匹配或激活,default.js 会作为该槽的默认渲染内容,避免页面出现空白。
  • 捕获未匹配状态:它允许你在并行路由的特定槽位中,为那些没有显式匹配到内容的 URL 提供一个优雅的降级方案。
  • not-found.js 的区别default.js 是为并行路由中未激活的槽位提供默认内容,而 not-found.js 是处理整个路由路径不匹配的情况。

使用场景:

  • 复杂仪表盘 :例如,一个仪表盘页面有多个独立的区域(如 @team@analytics),当用户直接访问仪表盘主页时,如果 @team 槽位没有特定的团队 ID,@team/default.js 可以显示"请选择一个团队"的提示。
  • 模态框/弹窗 :当一个模态框作为并行路由存在时,如果用户直接访问模态框的 URL,但没有通过触发器打开模态框,default.js 可以显示一个占位符或引导信息。

示例:

假设我们有一个并行路由 @team

jsx 复制代码
// app/@team/default.js
export default function TeamDefault() {
  return (
    <div style={{ border: '1px solid gray', padding: '10px', marginTop: '10px' }}>
      <h3>请从左侧导航选择一个团队</h3>
      <p>这里是团队信息的默认显示区域。</p>
    </div>
  );
}

// app/layout.js (部分)
export default function Layout({ children, team }) {
  return (
    <html>
      <body>
        <nav>...</nav>
        <main>{children}</main>
        {team} {/* 渲染并行路由槽 */}
      </body>
    </html>
  );
}

// app/page.js (部分)
export default function Page() {
  return (
    <div>
      <h1>欢迎来到仪表盘</h1>
      {/* 其他内容 */}
    </div>
  );
}

// app/@team/team-a/page.js
export default function TeamAPage() {
  return <div>这是 Team A 的详细信息</div>;
}

// 当用户访问 / 时,如果 @team 槽位没有被激活,TeamDefault 组件会被渲染。
// 当用户访问 /team-a 时,TeamAPage 组件会被渲染。

总结与思考

Next.js App Router 通过引入服务器组件、约定式特殊文件和简化的数据获取方式,为构建高性能、可扩展的现代 Web 应用提供了强大的工具。理解其项目结构和核心概念是充分利用 Next.js 15 强大功能的基石。

  • 服务器优先:默认服务器组件,减少客户端 JS 负担。
  • 约定优于配置:特殊文件简化了路由和 UI 逻辑。
  • 灵活的数据获取 :在服务器组件中直接 fetch 数据,Next.js 自动优化。
  • 性能与体验:通过流式渲染、错误边界和加载状态提升用户体验。

🔗 相关资源

相关推荐
百万蹄蹄向前冲3 小时前
秋天的第一口代码,Trae SOLO开发体验
前端·程序员·trae
努力奋斗13 小时前
VUE-第二季-02
前端·javascript·vue.js
路由侠内网穿透3 小时前
本地部署 SQLite 数据库管理工具 SQLite Browser ( Web ) 并实现外部访问
运维·服务器·开发语言·前端·数据库·sqlite
一只韩非子4 小时前
程序员太难了!Claude 用不了?两招解决!
前端·claude·cursor
Sane4 小时前
react函数组件怎么模拟类组件生命周期?一个 useEffect 搞定
前端·javascript·react.js
gnip4 小时前
可重试接口请求
前端·javascript
若梦plus4 小时前
模块化与package.json
前端
烛阴4 小时前
Aspect Ratio -- 宽高比
前端·webgl
若梦plus4 小时前
Node.js中util.promisify原理分析
前端·node.js