在 Next.js 中解锁可扩展的动态 UI。我们的综合指南涵盖用于组织项目的路由组 (Route Groups) 和用于复杂仪表板的并行路由 (Parallel Routes)。立即提升您的开发水平!
精通 Next.js App Router:深入解析路由组与并行路由架构
Next.js App Router 的发布标志着开发者使用这款流行的 React 框架构建 Web 应用程序的方式发生了范式转变。App Router 摒弃了 Pages Router 基于文件的约定,引入了一种更强大、更灵活、以服务器为中心的模型。这一演进使我们能够以更强的控制力和组织性来创建高度复杂和高性能的用户界面。其中最具变革性的功能是路由组 (Route Groups) 和并行路由 (Parallel Routes)。
对于旨在构建企业级应用程序的开发者来说,掌握这两个概念不仅有益,而且至关重要。它们解决了与布局管理、路由组织以及创建动态多面板界面(如仪表板)相关的常见架构挑战。本指南为全球开发者全面探索了路由组和并行路由,从基础概念到高级实现策略和最佳实践。
理解 Next.js App Router:快速回顾
在我们深入探讨细节之前,让我们简要回顾一下 App Router 的核心原则。其架构建立在一个基于目录的系统上,其中文件夹定义 URL 段。这些文件夹中的特殊文件定义了该段的 UI 和行为:
page.js
: 路由的主要 UI 组件,使其可公开访问。layout.js
: 一个包裹子布局或页面的 UI 组件。对于在多个路由中共享 UI(如页头和页脚)至关重要。loading.js
: 一个可选的 UI,用于在页面内容加载时显示,基于 React Suspense 构建。error.js
: 一个可选的 UI,用于在发生错误时显示,创建了健壮的错误边界。
这种结构,结合了 React 服务器组件 (RSCs) 的默认使用,鼓励了一种服务器优先的方法,可以显著提高性能和数据获取模式。路由组和并行路由是建立在此基础上的高级约定。
揭秘路由组:为清晰性和可扩展性组织您的项目
随着应用程序的增长,路由的数量可能会变得难以管理。您可能有一组用于营销的页面,另一组用于用户身份验证,第三组用于核心应用程序仪表板。从逻辑上讲,这些是独立的区域,但您如何在文件系统中组织它们而不使您的 URL 变得混乱?这正是路由组要解决的问题。
什么是路由组?
路由组是一种在不影响 URL 结构的情况下,将文件和路由段组织成逻辑分组的机制。您可以通过将文件夹名称用括号括起来创建路由组,例如 (marketing)
或 (app)
。
括号内的文件夹名称纯粹用于组织目的。Next.js 在确定 URL 路径时会完全忽略它。例如,位于 app/(marketing)/about/page.js
的文件将在 URL /about
处提供服务,而不是 /(marketing)/about
。
路由组的主要用例和优势
虽然简单的组织是一个优点,但路由组的真正威力在于它们能够将您的应用程序划分为具有不同共享布局的区域。
1. 为路由段创建不同的布局
这是最常见也是最强大的用例。想象一个 Web 应用程序有两个主要部分:
- 一个面向公众的营销网站(首页、关于、定价),具有全局页头和页脚。
- 一个私有的、需要认证的用户仪表板(仪表板、设置、个人资料),带有侧边栏、用户特定的导航和不同的整体结构。
如果没有路由组,为这些部分应用不同的根布局会很复杂。有了路由组,这一切都变得非常直观。您可以在每个组内创建一个唯一的 layout.js
文件。
以下是此场景的典型文件结构:
app/
├── (marketing)/
│ ├── layout.js // 带有营销页头/页脚的公共布局
│ ├── page.js // 在 '/' 处渲染
│ └── about/
│ └── page.js // 在 '/about' 处渲染
├── (app)/
│ ├── layout.js // 带有侧边栏的仪表板布局
│ ├── dashboard/
│ │ └── page.js // 在 '/dashboard' 处渲染
│ └── settings/
│ └── page.js // 在 '/settings' 处渲染
└── layout.js // 根布局 (例如,用于 <html> 和 <body> 标签)
在这个架构中:
(marketing)
组内的任何路由都将被(marketing)/layout.js
包裹。(app)
组内的任何路由都将被(app)/layout.js
包裹。- 两个组都共享根
app/layout.js
,这非常适合定义全局 HTML 结构。
2. 将某个路由段从共享布局中分离
有时,某个特定页面或部分需要完全脱离父布局。一个常见的例子是结账流程或一个特殊的登陆页面,它们不应该有主站点的导航。您可以通过将该路由放置在一个不共享上层布局的组中来实现这一点。虽然这听起来很复杂,但它仅仅意味着给一个路由组自己的顶级 layout.js
,该布局不渲染来自根布局的 `children`。
实践示例:构建一个多布局应用
让我们来构建一个上述营销/应用结构的最小化版本。
1. 根布局 (app/layout.js
)
这个布局非常精简,并应用于每一个页面。它定义了基本的 HTML 结构。
// app/layout.js
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
2. 营销布局 (app/(marketing)/layout.js
)
这个布局包括一个面向公众的页头和页脚。
// app/(marketing)/layout.js
export default function MarketingLayout({ children }) {
return (
<div>
<header>Marketing Header</header>
<main>{children}</main>
<footer>Marketing Footer</footer>
</div>
);
}
3. 应用仪表板布局 (app/(app)/layout.js
)
这个布局有不同的结构,为认证用户提供了一个侧边栏。
// app/(app)/layout.js
export default function AppLayout({ children }) {
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: '200px', borderRight: '1px solid #ccc' }}>
Dashboard Sidebar
</aside>
<main style={{ flex: 1, padding: '20px' }}>{children}</main>
</div>
);
}
通过这种结构,导航到 /about
将会使用 `MarketingLayout` 渲染页面,而导航到 /dashboard
则会使用 `AppLayout` 渲染。URL 保持干净和语义化,而我们的项目文件结构则完美地组织起来并具有可扩展性。
使用并行路由解锁动态 UI
虽然路由组有助于组织应用程序的不同部分,但并行路由解决了另一个挑战:在单个布局中显示多个独立的页面视图。这对于复杂的仪表板、社交媒体信息流或任何需要同时渲染和管理不同面板的 UI 来说是一个常见的需求。
什么是并行路由?
并行路由允许您在同一个布局中同时渲染一个或多个页面。这些路由使用一种称为插槽 (slots) 的特殊文件夹约定来定义。插槽通过 @folderName
语法创建。它们不属于 URL 结构;相反,它们会自动作为 props 传递给最近的共享父级 `layout.js` 文件。
例如,如果您有一个布局需要并排显示团队活动信息流和分析图表,您可以定义两个插槽:`@team` 和 `@analytics`。
核心思想:插槽 (Slots)
可以将插槽想象成布局中的命名占位符。布局文件明确接受这些插槽作为 props,并决定在哪里渲染它们。
考虑这个布局组件:
// 一个接受 'team' 和 'analytics' 两个插槽的布局
export default function DashboardLayout({ children, team, analytics }) {
return (
<div>
{children}
<div style={{ display: 'flex' }}>
{team}
{analytics}
</div>
</div>
);
}
在这里,`children`、`team` 和 `analytics` 都是插槽。`children` 是一个隐式插槽,对应于目录中标准的 `page.js`。`team` 和 `analytics` 是显式插槽,必须在文件系统中使用 `@` 前缀创建。
主要特性与优势
- 独立的路由处理: 每个并行路由(插槽)都可以有自己的加载和错误状态。这意味着您的分析面板可以显示加载指示器,而团队信息流已经渲染完成,从而带来更好的用户体验。
- 条件渲染: 您可以根据某些条件(如用户认证状态或许可)以编程方式决定渲染哪些插槽。
- 子导航: 每个插槽都可以独立导航,而不影响其他插槽。这非常适合选项卡式界面或仪表板,其中一个面板的状态与另一个面板完全分离。
真实场景:构建一个复杂的仪表板
让我们在 URL /dashboard
设计一个仪表板。它将有一个主内容区、一个团队活动面板和一个性能分析面板。
文件结构:
app/
└── dashboard/
├── @analytics/
│ ├── page.js // 分析插槽的 UI
│ └── loading.js // 专用于分析的加载 UI
├── @team/
│ └── page.js // 团队插槽的 UI
├── layout.js // 编排插槽的布局
└── page.js // 隐式的 'children' 插槽 (主内容)
1. 仪表板布局 (app/dashboard/layout.js
)
这个布局接收并安排这三个插槽。
// app/dashboard/layout.js
export default function DashboardLayout({ children, analytics, team }) {
const isLoggedIn = true; // 用真实的认证逻辑替换
return isLoggedIn ? (
<div>
<h1>Main Dashboard</h1>
{children}
<div style={{ marginTop: '20px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
<div style={{ border: '1px solid blue', padding: '10px' }}>
<h2>Team Activity</h2>
{team}
</div>
<div style={{ border: '1px solid green', padding: '10px' }}>
<h2>Performance Analytics</h2>
{analytics}
</div>
</div>
</div>
) : (
<div>Please log in to view the dashboard.</div>
);
}
2. 插槽页面 (例如,app/dashboard/@analytics/page.js
)
每个插槽的 `page.js` 文件包含该特定面板的 UI。
// app/dashboard/@analytics/page.js
async function getAnalyticsData() {
// 模拟网络请求
await new Promise(resolve => setTimeout(resolve, 3000));
return { views: '1.2M', revenue: '$50,000' };
}
export default async function AnalyticsPage() {
const data = await getAnalyticsData();
return (
<div>
<p>Page Views: {data.views}</p>
<p>Revenue: {data.revenue}</p>
</div>
);
}
// app/dashboard/@analytics/loading.js
export default function Loading() {
return <p>Loading analytics data...</p>;
}
通过此设置,当用户导航到 /dashboard
时,Next.js 将渲染 `DashboardLayout`。该布局将接收来自 dashboard/page.js
、dashboard/@team/page.js
和 dashboard/@analytics/page.js
的渲染内容作为 props,并相应地放置它们。关键是,分析面板将显示其自己的 `loading.js` 状态 3 秒钟,而不会阻塞仪表板其余部分的渲染。
使用 `default.js` 处理不匹配的路由
一个关键问题出现了:如果 Next.js 无法为当前 URL 检索到插槽的活动状态,会发生什么?例如,在初始加载或页面重新加载时,URL 可能是 /dashboard
,它没有为 @team
或 @analytics
插槽内显示什么内容提供具体指令。默认情况下,Next.js 会渲染一个 404 错误。
为防止这种情况,我们可以通过在并行路由内创建 default.js
文件来提供一个后备 UI。
示例:
// app/dashboard/@analytics/default.js
export default function DefaultAnalyticsPage() {
return (
<div>
<p>No analytics data selected.</p>
</div>
);
}
现在,如果分析插槽不匹配,Next.js 将渲染 `default.js` 的内容,而不是 404 页面。这对于创建流畅的用户体验至关重要,尤其是在复杂并行路由设置的初始加载时。
结合路由组与并行路由实现高级架构
当您结合其各项功能时,App Router 的真正威力才能得以体现。路由组和并行路由可以完美地协同工作,创建出复杂且高度组织化的应用程序架构。
用例:多模态内容查看器
想象一个像媒体库或文档查看器这样的平台,用户可以查看一个项目,但也可以打开一个模态窗口查看其详细信息,而不会丢失背景页面的上下文。这通常被称为“拦截路由”,是一种基于并行路由构建的强大模式。
让我们创建一个照片库。当您点击一张照片时,它会在一个模态框中打开。但是,如果您刷新页面或直接导航到该照片的 URL,它应该显示该照片的专用页面。
文件结构:
app/
├── @modal/(..)(..)photos/[id]/page.js // 用于模态框的拦截路由
├── photos/
│ └── [id]/
│ └── page.js // 专用的照片页面
├── layout.js // 接收 @modal 插槽的根布局
└── page.js // 主照片库页面
解释:
- 我们创建了一个名为
@modal
的并行路由插槽。 - 看起来很奇怪的路径
(..)(..)photos/[id]
使用了一种称为“捕获所有段”的约定,以匹配从根目录向上两级的photos/[id]
路由。 - 当用户从主照片库页面(
/
)导航到一张照片时,Next.js 会拦截此导航,并在@modal
插槽内渲染模态框的页面,而不是执行完整的页面导航。 - 主照片库页面在布局的
children
prop 中仍然可见。 - 如果用户直接访问
/photos/123
,拦截不会触发,位于photos/[id]/page.js
的专用页面会正常渲染。
这种模式结合了并行路由(@modal
插槽)和高级路由约定,创建了一种无缝的用户体验,而这种体验如果手动实现会非常复杂。
最佳实践与常见陷阱
路由组最佳实践
- 使用描述性名称: 选择有意义的名称,如
(auth)
、(marketing)
或(protected)
,使您的项目结构具有自文档性。 - 尽可能保持扁平: 避免路由组的过度嵌套。一个更扁平的结构通常更容易理解和维护。
- 牢记其目的: 使用它们进行布局分区和组织,而不是用于创建 URL 段。
并行路由最佳实践
- 始终提供 `default.js`: 对于任何不简单的并行路由使用,都应包含一个 `default.js` 文件,以优雅地处理初始加载和不匹配的状态。
- 利用精细的加载状态: 在每个插槽的目录中放置一个 `loading.js` 文件,为用户提供即时反馈并防止 UI 瀑布流。
- 用于独立的 UI: 当每个插槽的内容真正独立时,并行路由才能发挥最大作用。如果面板之间紧密相连,通过单个组件树向下传递 props 可能是一个更简单的解决方案。
需要避免的常见陷阱
- 忘记约定: 一个常见的错误是忘记路由组的括号
()
或并行路由插槽的 at 符号@
。这将导致它们被视为普通的 URL 段。 - 缺少 `default.js`: 并行路由最常见的问题是看到意外的 404 错误,因为没有为不匹配的插槽提供后备的 `default.js`。
- 误解 `children`: 在使用并行路由的布局中,请记住 `children` 只是其中一个插槽,它隐式映射到同一目录中的 `page.js` 或嵌套布局。
结论:构建 Web 应用的未来
Next.js App Router 及其功能,如路由组和并行路由,为现代 Web 开发提供了坚实且可扩展的基础。路由组为组织代码和应用不同布局提供了一种优雅的解决方案,而不会影响 URL 的语义。并行路由解锁了构建具有独立状态的动态多面板界面的能力,这在以前只能通过复杂的客户端状态管理来实现。
通过理解和结合这些强大的架构模式,您可以超越简单的网站,开始构建能够满足当今用户需求的复杂、高性能和可维护的应用程序。学习曲线可能比传统的 Pages Router 更陡峭,但在应用程序架构和用户体验方面的回报是巨大的。在您的下一个项目中开始尝试这些概念,解锁 Next.js 的全部潜力。