通过我们对基于文件的路由的深度指南,释放 Next.js App Router 的强大功能。学习如何构建您的应用程序、创建动态路由、处理布局等。
Next.js App Router:基于文件的路由全面指南
在 Next.js 13 中引入并在后续版本中成为标准的 Next.js App Router,彻底改变了我们构建和导航应用程序的方式。它引入了一个强大而直观的基于文件的路由系统,简化了开发,提高了性能,并增强了整体的开发者体验。本综合指南将深入探讨 App Router 的文件路由,为您提供构建健壮且可扩展的 Next.js 应用程序所需的知识和技能。
什么是基于文件的路由?
基于文件的路由是一种路由系统,其中应用程序的路由结构直接由文件和目录的组织方式决定。在 Next.js App Router 中,您通过在 `app` 目录中创建文件来定义路由。每个文件夹代表一个路由段,而这些文件夹中的特殊文件则定义了该路由段将如何被处理。这种方法有几个优点:
- 直观的结构: 文件系统反映了应用程序的路由结构,使其易于理解和导航。
- 自动路由: Next.js 会根据您的文件结构自动生成路由,无需手动配置。
- 代码共置: 路由处理程序和 UI 组件位于一起,提高了代码的组织性和可维护性。
- 内置功能: App Router 为布局、动态路由、数据获取等提供了内置支持,简化了复杂的路由场景。
App Router 入门
要使用 App Router,您需要创建一个新的 Next.js 项目或迁移一个现有项目。请确保您使用的是 Next.js 13 或更高版本。
创建新项目:
您可以使用以下命令创建一个带有 App Router 的新 Next.js 项目:
npx create-next-app@latest my-app --example with-app
迁移现有项目:
要迁移现有项目,您需要将页面从 `pages` 目录移动到 `app` 目录。您可能需要相应地调整路由逻辑。Next.js 提供了一份迁移指南来帮助您完成此过程。
基于文件的路由的核心概念
App Router 引入了几个特殊的文件和约定,用于定义路由的处理方式:
1. `app` 目录
`app` 目录是您应用程序路由的根目录。此目录中的所有文件和文件夹都将用于生成路由。`app` 目录之外的任何内容(例如,如果您正在迁移,则为 `pages` 目录)都将被 App Router 忽略。
2. `page.js` 文件
`page.js`(或 `page.jsx`、`page.ts`、`page.tsx`)文件是 App Router 最基本的部分。它定义了将为特定路由段渲染的 UI 组件。对于您希望直接访问的任何路由段,它都是一个必需文件。
示例:
如果您的文件结构如下:
app/
about/
page.js
当用户导航到 `/about` 时,将渲染从 `app/about/page.js` 导出的组件。
// app/about/page.js
import React from 'react';
export default function AboutPage() {
return (
<div>
<h1>关于我们</h1>
<p>了解更多关于我们公司的信息。</p>
</div>
);
}
3. `layout.js` 文件
`layout.js`(或 `layout.jsx`、`layout.ts`、`layout.tsx`)文件定义了一个在路由段内的多个页面之间共享的 UI。布局对于创建一致的页眉、页脚、侧边栏以及应在多个页面上出现的其他元素非常有用。
示例:
假设您想为 `/about` 页面和假设的 `/about/team` 页面添加一个页眉。您可以在 `app/about` 目录中创建一个 `layout.js` 文件:
// app/about/layout.js
import React from 'react';
export default function AboutLayout({ children }) {
return (
<div>
<header>
<h1>关于我们公司</h1>
</header>
<main>{children}</main>
</div>
);
}
`children` 属性将被替换为由同一目录或任何嵌套目录中的 `page.js` 文件渲染的 UI。
4. `template.js` 文件
`template.js` 文件与 `layout.js` 类似,但它会为每个子路由创建一个新的组件实例。这对于您希望在子路由之间导航时维持组件状态或防止重新渲染的场景非常有用。与布局不同,模板在导航时会重新渲染。 使用模板非常适合在导航时为元素添加动画效果。
示例:
// app/template.js
'use client'
import { useState } from 'react'
export default function Template({ children }) {
const [count, setCount] = useState(0)
return (
<main>
<p>模板: {count}</p>
<button onClick={() => setCount(count + 1)}>更新模板</button>
{children}
</main>
)
}
5. `loading.js` 文件
`loading.js`(或 `loading.jsx`、`loading.ts`、`loading.tsx`)文件允许您创建一个在路由段加载时显示的加载 UI。这在获取数据或执行其他异步操作时,有助于提供更好的用户体验。
示例:
// app/about/loading.js
import React from 'react';
export default function Loading() {
return <p>正在加载关于信息...</p>;
}
当用户导航到 `/about` 时,将显示 `Loading` 组件,直到 `page.js` 组件完全渲染。
6. `error.js` 文件
`error.js`(或 `error.jsx`、`error.ts`、`error.tsx`)文件允许您创建一个自定义错误 UI,在路由段内发生错误时显示。这有助于提供更友好的错误消息,并防止整个应用程序崩溃。
示例:
// app/about/error.js
'use client'
import React from 'react';
export default function Error({ error, reset }) {
return (
<div>
<h2>发生了一个错误!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>再试一次</button>
</div>
);
}
如果在渲染 `/about` 页面时发生错误,将显示 `Error` 组件。`error` 属性包含有关错误的信息,而 `reset` 函数允许用户尝试重新加载页面。
7. 路由组
路由组 `(groupName)` 允许您组织路由而不影响 URL 结构。它们通过将文件夹名称用括号括起来创建。这对于组织布局和共享组件特别有帮助。
示例:
app/
(marketing)/
about/
page.js
contact/
page.js
(shop)/
products/
page.js
在此示例中,`about` 和 `contact` 页面被分组在 `marketing` 组下,而 `products` 页面则在 `shop` 组下。URL 分别保持为 `/about`、`/contact` 和 `/products`。
8. 动态路由
动态路由允许您创建具有可变段的路由。这对于根据从数据库或 API 获取的数据显示内容非常有用。动态路由段通过将段名称用方括号括起来定义(例如,`[id]`)。
示例:
假设您想创建一个路由,用于根据其 ID 显示单个博客文章。您可以创建如下的文件结构:
app/
blog/
[id]/
page.js
`[id]` 段是一个动态段。当用户导航到像 `/blog/123` 或 `/blog/456` 这样的 URL 时,将渲染从 `app/blog/[id]/page.js` 导出的组件。`id` 参数的值将在组件的 `params` 属性中可用。
// app/blog/[id]/page.js
import React from 'react';
export default async function BlogPost({ params }) {
const { id } = params;
// 根据给定的 ID 获取博客文章数据
const post = await fetchBlogPost(id);
if (!post) {
return <p>未找到博客文章。</p>;
}
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
async function fetchBlogPost(id) {
// 模拟从数据库或 API 获取数据
return new Promise((resolve) => {
setTimeout(() => {
const posts = {
'123': { title: '我的第一篇博客文章', content: '这是我第一篇博客文章的内容。' },
'456': { title: '另一篇博客文章', content: '这是一些更精彩的内容。' },
};
resolve(posts[id] || null);
}, 500);
});
}
您也可以在路由中使用多个动态段。例如,您可以有一个像 `/blog/[category]/[id]` 这样的路由。
9. 捕获所有段
捕获所有段允许您创建匹配任意数量段的路由。这对于像创建 CMS(内容管理系统)这样的场景非常有用,其中 URL 结构由用户决定。捕获所有段通过在段名称前添加三个点来定义(例如,`[...slug]`)。
示例:
app/
docs/
[...slug]/
page.js
`[...slug]` 段将匹配 `/docs` 之后的任意数量的段。例如,它将匹配 `/docs/getting-started`、`/docs/api/users` 和 `/docs/advanced/configuration`。`slug` 参数的值将是一个包含匹配段的数组。
// app/docs/[...slug]/page.js
import React from 'react';
export default function DocsPage({ params }) {
const { slug } = params;
return (
<div>
<h1>文档</h1>
<p>Slug: {slug ? slug.join('/') : '没有 slug'}</p>
</div>
);
}
可选的捕获所有段可以通过将段名称放在双方括号中 `[[...slug]]` 来创建。这使得该路由段是可选的。 示例:
app/
blog/
[[...slug]]/
page.js
此设置将在 `/blog` 和 `/blog/any/number/of/segments` 处都渲染 page.js 组件。
10. 平行路由
平行路由允许您在同一个布局中同时渲染一个或多个页面。这对于复杂的布局(如仪表板)特别有用,其中页面的不同部分可以独立加载。平行路由使用 `@` 符号后跟一个插槽名称来定义(例如,`@sidebar`、`@main`)。
示例:
app/
@sidebar/
page.js // 侧边栏的内容
@main/
page.js // 主区域的内容
default.js // 必需:定义平行路由的默认布局
在使用平行路由时,`default.js` 文件是必需的。它定义了如何将不同的插槽组合以创建最终布局。
// app/default.js
export default function RootLayout({ children: { sidebar, main } }) {
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: '200px', backgroundColor: '#f0f0f0' }}>
{sidebar}
</aside>
<main style={{ flex: 1, padding: '20px' }}>
{main}
</main>
</div>
);
}
11. 拦截路由
拦截路由允许您在当前布局内加载来自应用程序不同部分的路由。这对于创建模态框、图片库以及其他应出现在现有页面内容之上的 UI 元素非常有用。拦截路由使用 `(..)` 语法定义,该语法指示要向上查找多少级目录树以找到被拦截的路由。
示例:
app/
(.)photos/
[id]/
page.js // 被拦截的路由
feed/
page.js // 显示照片模态框的页面
在此示例中,当用户在 `/feed` 页面上点击一张照片时,`app/(.)photos/[id]/page.js` 路由被拦截,并作为模态框显示在 `/feed` 页面之上。`(.)` 语法告诉 Next.js 向上查找一级(到 `app` 目录)以找到 `photos/[id]` 路由。
使用 App Router 进行数据获取
App Router 使用服务器组件和客户端组件为数据获取提供内置支持。服务器组件在服务器上渲染,而客户端组件在客户端渲染。这使您可以根据每个组件的需求选择最佳方法。
服务器组件
服务器组件是 App Router 中的默认设置。它们允许您直接在组件中获取数据,无需单独的 API 路由。这可以提高性能并简化代码。
示例:
// app/products/page.js
import React from 'react';
export default async function ProductsPage() {
const products = await fetchProducts();
return (
<div>
<h1>产品</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}
async function fetchProducts() {
// 模拟从数据库或 API 获取数据
return new Promise((resolve) => {
setTimeout(() => {
const products = [
{ id: 1, name: '产品 A' },
{ id: 2, name: '产品 B' },
{ id: 3, name: '产品 C' },
];
resolve(products);
}, 500);
});
}
在此示例中,`fetchProducts` 函数直接在 `ProductsPage` 组件内调用。该组件在服务器上渲染,并且在将 HTML 发送到客户端之前获取数据。
客户端组件
客户端组件在客户端渲染,并允许您使用客户端功能,如事件监听器、状态和浏览器 API。要使用客户端组件,您需要在文件顶部添加 `'use client'` 指令。
示例:
// app/counter/page.js
'use client'
import React, { useState } from 'react';
export default function CounterPage() {
const [count, setCount] = useState(0);
return (
<div>
<h1>计数器</h1>
<p>计数: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
在此示例中,`CounterPage` 组件是一个客户端组件,因为它使用了 `useState` 钩子。`'use client'` 指令告诉 Next.js 在客户端渲染此组件。
高级路由技术
App Router 提供了几种高级路由技术,可用于创建复杂和精密的应用程序。
1. 路由处理器
路由处理器允许您在 `app` 目录内创建 API 端点。这消除了对单独的 `pages/api` 目录的需求。路由处理器在名为 `route.js`(或 `route.ts`)的文件中定义,并导出处理不同 HTTP 方法(例如,`GET`、`POST`、`PUT`、`DELETE`)的函数。
示例:
// app/api/users/route.js
import { NextResponse } from 'next/server'
export async function GET(request) {
// 模拟从数据库获取用户
const users = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' },
];
return NextResponse.json(users);
}
export async function POST(request) {
const body = await request.json()
console.log('收到的数据:', body)
return NextResponse.json({ message: '用户已创建' }, { status: 201 })
}
此示例在 `/api/users` 处定义了一个路由处理器,可处理 `GET` 和 `POST` 请求。`GET` 函数返回用户列表,`POST` 函数创建一个新用户。
2. 带有多个布局的路由组
您可以将路由组与布局相结合,为应用程序的不同部分创建不同的布局。这对于您希望为网站的不同部分设置不同页眉或侧边栏的场景非常有用。
示例:
app/
(marketing)/
layout.js // 市场营销布局
about/
page.js
contact/
page.js
(admin)/
layout.js // 管理员布局
dashboard/
page.js
在此示例中,`about` 和 `contact` 页面将使用 `marketing` 布局,而 `dashboard` 页面将使用 `admin` 布局。
3. 中间件
中间件允许您在请求被应用程序处理之前运行代码。这对于身份验证、授权、日志记录以及根据用户位置或设备重定向用户等任务非常有用。
中间件在项目根目录下的一个名为 `middleware.js`(或 `middleware.ts`)的文件中定义。
示例:
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
// 检查用户是否已通过身份验证
const isAuthenticated = false; // 使用您的身份验证逻辑替换
if (!isAuthenticated && request.nextUrl.pathname.startsWith('/admin')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
// 参阅下方的“匹配路径”以了解更多信息
export const config = {
matcher: '/admin/:path*',
}
此示例定义了一个中间件,在允许用户访问 `/admin` 下的任何路由之前检查用户是否已通过身份验证。如果用户未通过身份验证,他们将被重定向到 `/login` 页面。
文件路由的最佳实践
要充分利用 App Router 的文件路由系统,请考虑以下最佳实践:
- 保持文件结构井然有序: 使用有意义的文件夹名称,并将相关文件分组在一起。
- 对共享 UI 使用布局: 为页眉、页脚、侧边栏以及在多个页面之间共享的其他元素创建布局。
- 使用加载 UI: 为获取数据或执行其他异步操作的路由提供加载 UI。
- 优雅地处理错误: 创建自定义错误 UI,以便在发生错误时提供更好的用户体验。
- 使用路由组进行组织: 使用路由组来组织您的路由,而不影响 URL 结构。
- 利用服务器组件提升性能: 使用服务器组件在服务器上获取数据和渲染 UI,从而提高性能和 SEO。
- 在必要时使用客户端组件: 当您需要使用客户端功能(如事件监听器、状态和浏览器 API)时,使用客户端组件。
Next.js App Router 国际化示例
Next.js App Router 通过基于文件的路由简化了国际化(i18n)。以下是您可以如何有效地实现 i18n 的方法:
1. 子路径路由
使用子路径根据区域设置来组织您的路由。例如:
app/
[locale]/
page.tsx // 对应区域设置的主页
about/
page.tsx // 对应区域设置的关于页面
// app/[locale]/page.tsx
import { getTranslations } from './dictionaries';
export default async function HomePage({ params: { locale } }) {
const t = await getTranslations(locale);
return (<h1>{t.home.title}</h1>);
}
// dictionaries.js
const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
es: () => import('./dictionaries/es.json').then((module) => module.default),
};
export const getTranslations = async (locale) => {
try {
return dictionaries[locale]() ?? dictionaries.en();
} catch (error) {
console.error(`无法加载区域设置 ${locale} 的翻译`, error);
return dictionaries.en();
}
};
在此设置中,`[locale]` 动态路由段处理不同的区域设置(例如 `/en`、`/es`)。翻译内容会根据区域设置动态加载。
2. 域名路由
对于更高级的方法,您可以为每个区域设置使用不同的域名或子域名。这通常需要与您的托管提供商进行额外的配置。
3. 用于区域设置检测的中间件
使用中间件自动检测用户的首选区域设置并相应地重定向他们。
// middleware.js
import { NextResponse } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
let locales = ['en', 'es', 'fr'];
function getLocale(request) {
const negotiatorHeaders = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
let languages = new Negotiator({ headers: negotiatorHeaders }).languages();
try {
return match(languages, locales, 'en'); // 使用 "en" 作为默认区域设置
} catch (error) {
console.error("匹配区域设置时出错:", error);
return 'en'; // 如果匹配失败,则回退到英语
}
}
export function middleware(request) {
const pathname = request.nextUrl.pathname;
const pathnameIsMissingLocale = locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
);
if (pathnameIsMissingLocale) {
const locale = getLocale(request);
return NextResponse.redirect(
new URL(
`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`,
request.url
)
);
}
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
此中间件检查请求的路径是否带有区域设置前缀。如果没有,它会使用 `Accept-Language` 标头检测用户的首选区域设置,并将他们重定向到相应的特定区域设置路径。像 `@formatjs/intl-localematcher` 和 `negotiator` 这样的库用于处理区域设置协商。
Next.js App Router 与全球可访问性
创建全球可访问的 Web 应用程序需要仔细考虑可访问性(a11y)原则。Next.js App Router 为构建可访问的体验提供了坚实的基础,但实施最佳实践以确保您的应用程序对所有人可用至关重要,无论他们的能力如何。
关键的可访问性考虑因素
- 语义化 HTML: 使用语义化 HTML 元素(例如 `<article>`、`<nav>`、`<aside>`、`<main>`)来构建您的内容。这为辅助技术提供了意义,并帮助用户更容易地导航您的网站。
- ARIA 属性: 使用 ARIA(无障碍丰富互联网应用)属性来增强自定义组件和小部件的可访问性。ARIA 属性为辅助技术提供有关元素角色、状态和属性的附加信息。
- 键盘导航: 确保所有交互式元素都可以通过键盘访问。用户应该能够使用 `Tab` 键在您的应用程序中导航,并使用 `Enter` 或 `Space` 键与元素进行交互。
- 颜色对比度: 在文本和背景之间使用足够的颜色对比度,以确保视力障碍用户的可读性。Web 内容可访问性指南(WCAG)建议普通文本的对比度至少为 4.5:1,大文本为 3:1。
- 图像替代文本: 为所有图像提供描述性的替代文本。替代文本为图像提供了文本替代方案,可由屏幕阅读器读取。
- 表单标签: 使用 `<label>` 元素将表单标签与其相应的输入字段关联起来。这使用户清楚地知道每个字段中期望填写什么信息。
- 屏幕阅读器测试: 使用屏幕阅读器测试您的应用程序,以确保它对视力障碍用户是可访问的。流行的屏幕阅读器包括 NVDA、JAWS 和 VoiceOver。
在 Next.js App Router 中实现可访问性
- 使用 Next.js Link 组件: 使用 `<Link>` 组件进行导航。它提供了内置的可访问性功能,例如预取和焦点管理。
- 焦点管理: 在页面之间导航或打开模态框时,确保焦点得到妥善管理。焦点应设置在新页面或模态框上最合乎逻辑的元素上。
- 可访问的自定义组件: 在创建自定义组件时,请遵循上述原则确保其可访问。使用语义化 HTML、ARIA 属性和键盘导航,使您的组件对每个人都可用。
- 代码检查和测试: 使用像 ESLint 这样的代码检查工具及其可访问性插件来识别代码中潜在的可访问性问题。此外,使用自动化测试工具来测试您的应用程序是否存在可访问性违规。
结论
Next.js App Router 的基于文件的路由系统提供了一种强大而直观的方式来构建和导航您的应用程序。通过理解本指南中概述的核心概念和最佳实践,您可以构建健壮、可扩展且可维护的 Next.js 应用程序。尝试 App Router 的不同功能,发现它如何简化您的开发工作流程并改善用户体验。