Розкрийте можливості App Router у Next.js з нашим детальним посібником з файлової маршрутизації. Дізнайтеся, як структурувати ваш застосунок, створювати динамічні маршрути, керувати макетами тощо.
App Router у Next.js: Повний посібник з файлової маршрутизації
App Router у Next.js, представлений у 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" (всеохопні)
Сегменти "Catch-all" дозволяють створювати маршрути, які відповідають будь-якій кількості сегментів. Це корисно для таких сценаріїв, як створення CMS, де структура URL визначається користувачем. Сегменти "Catch-all" визначаються шляхом додавання трьох крапок перед назвою сегмента (наприклад, `[...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>
);
}
Опціональні сегменти "catch-all" можна створити, додавши назву сегмента в подвійні квадратні дужки `[[...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 шукати на один рівень вище (до директорії `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. Обробники маршрутів (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: '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 (проміжне ПЗ)
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` або `Space`.
- Контрастність кольорів: Використовуйте достатній контраст кольорів між текстом та фоном, щоб забезпечити читабельність для користувачів з вадами зору. Рекомендації щодо доступності веб-контенту (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 і відкрийте для себе, як він може спростити ваш робочий процес розробки та покращити досвід користувача.