Раскройте всю мощь Next.js App Router с нашим подробным руководством по файловой маршрутизации. Узнайте, как структурировать приложение, создавать динамические маршруты, работать с макетами и многое другое.
Next.js App Router: Полное руководство по файловой маршрутизации
Next.js App Router, представленный в Next.js 13 и ставший стандартом в более поздних версиях, кардинально меняет наш подход к структурированию и навигации в приложениях. Он вводит мощную и интуитивно понятную файловую систему маршрутизации, которая упрощает разработку, улучшает производительность и повышает общее удобство для разработчиков. Это всеобъемлющее руководство подробно рассмотрит файловую маршрутизацию App Router, предоставив вам знания и навыки для создания надежных и масштабируемых приложений на Next.js.
Что такое файловая маршрутизация?
Файловая маршрутизация — это система маршрутизации, в которой структура маршрутов вашего приложения напрямую определяется организацией ваших файлов и каталогов. В Next.js App Router вы определяете маршруты, создавая файлы в каталоге `app`. Каждая папка представляет собой сегмент маршрута, а специальные файлы в этих папках определяют, как этот сегмент будет обрабатываться. Такой подход имеет несколько преимуществ:
- Интуитивно понятная структура: Файловая система отражает структуру маршрутов приложения, что делает ее легкой для понимания и навигации.
- Автоматическая маршрутизация: Next.js автоматически генерирует маршруты на основе вашей файловой структуры, устраняя необходимость в ручной настройке.
- Коллокация кода: Обработчики маршрутов и компоненты UI находятся вместе, что улучшает организацию и поддерживаемость кода.
- Встроенные функции: App Router предоставляет встроенную поддержку макетов, динамических маршрутов, получения данных и многого другого, упрощая сложные сценарии маршрутизации.
Начало работы с App Router
Чтобы использовать App Router, вам нужно создать новый проект Next.js или мигрировать существующий. Убедитесь, что вы используете Next.js версии 13 или выше.
Создание нового проекта:
Вы можете создать новый проект Next.js с App Router, используя следующую команду:
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
Компонент, экспортируемый из `app/about/page.js`, будет отображен, когда пользователь перейдет на `/about`.
// 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`. Вы можете создать файл `layout.js` в каталоге `app/about`:
// app/about/layout.js
import React from 'react';
export default function AboutLayout({ children }) {
return (
<div>
<header>
<h1>О нашей компании</h1>
</header>
<main>{children}</main>
</div>
);
}
Свойство `children` будет заменено на UI, отображаемый файлом `page.js` в том же каталоге или в любых вложенных каталогах.
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]` является динамическим. Компонент, экспортируемый из `app/blog/[id]/page.js`, будет отображен, когда пользователь перейдет по URL, такому как `/blog/123` или `/blog/456`. Значение параметра `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. Всеохватывающие сегменты
Всеохватывающие сегменты (catch-all) позволяют создавать маршруты, которые соответствуют любому количеству сегментов. Это полезно для таких сценариев, как создание 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
Эта настройка будет отображать компонент page.js как по адресу `/blog`, так и по адресу `/blog/any/number/of/segments`.
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 искать маршрут `photos/[id]` на один уровень выше (в каталоге `app`).
Получение данных с помощью 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: 'Продукт А' },
{ id: 2, name: 'Продукт Б' },
{ id: 3, name: 'Продукт В' },
];
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. Обработчики маршрутов (Route Handlers)
Обработчики маршрутов позволяют создавать конечные точки API в вашем каталоге `app`. Это устраняет необходимость в отдельном каталоге `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: 'Иван Иванов' },
{ id: 2, name: 'Мария Петрова' },
];
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 (Промежуточное ПО)
Middleware позволяет вам выполнять код до того, как запрос будет обработан вашим приложением. Это полезно для таких задач, как аутентификация, авторизация, логирование и перенаправление пользователей в зависимости от их местоположения или устройства.
Middleware определяется в файле с именем `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();
}
// См. "Matching Paths" ниже, чтобы узнать больше
export const config = {
matcher: '/admin/:path*',
}
Этот пример определяет middleware, которое проверяет, аутентифицирован ли пользователь, прежде чем разрешить ему доступ к любому маршруту в `/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 для определения локали
Используйте middleware для автоматического определения предпочитаемой локали пользователя и соответствующего перенаправления.
// 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).*)',
],
};
Это middleware проверяет, есть ли в запрашиваемом пути префикс локали. Если нет, оно определяет предпочитаемую локаль пользователя с помощью заголовка `Accept-Language` и перенаправляет его на соответствующий путь с указанием локали. Для обработки согласования локалей используются библиотеки, такие как `@formatjs/intl-localematcher` и `negotiator`.
Next.js App Router и глобальная доступность
Создание глобально доступных веб-приложений требует тщательного учета принципов доступности (a11y). Next.js App Router предоставляет прочную основу для создания доступных интерфейсов, но для того, чтобы ваше приложение было пригодно для использования всеми, независимо от их способностей, необходимо применять лучшие практики.
Ключевые аспекты доступности
- Семантический HTML: Используйте семантические HTML-элементы (например, `<article>`, `<nav>`, `<aside>`, `<main>`) для структурирования вашего контента. Это придает смысл вспомогательным технологиям и помогает пользователям легче ориентироваться на вашем сайте.
- Атрибуты ARIA: Используйте атрибуты ARIA (Accessible Rich Internet Applications) для повышения доступности пользовательских компонентов и виджетов. Атрибуты ARIA предоставляют вспомогательным технологиям дополнительную информацию о роли, состоянии и свойствах элементов.
- Клавиатурная навигация: Убедитесь, что все интерактивные элементы доступны с клавиатуры. Пользователи должны иметь возможность перемещаться по вашему приложению с помощью клавиши `Tab` и взаимодействовать с элементами с помощью клавиш `Enter` или `Пробел`.
- Цветовой контраст: Используйте достаточный цветовой контраст между текстом и фоном для обеспечения читаемости для пользователей с нарушениями зрения. Руководство по доступности веб-контента (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 и откройте для себя, как он может упростить ваш рабочий процесс разработки и улучшить пользовательский опыт.