Български

Отключете силата на Next.js App Router с нашето подробно ръководство за файлово-базирано маршрутизиране. Научете как да структурирате приложението си, да създавате динамични маршрути, да управлявате оформления и много други.

Next.js App Router: Цялостно ръководство за файлово-базирано маршрутизиране

Next.js App Router, въведен в Next.js 13 и превърнал се в стандарт в по-късните версии, революционизира начина, по който структурираме и навигираме в приложенията. Той въвежда мощна и интуитивна файлово-базирана система за маршрутизиране, която опростява разработката, подобрява производителността и подобрява цялостното преживяване на разработчика. Това подробно ръководство ще се потопи дълбоко във файлово-базираното маршрутизиране на App Router, предоставяйки ви знанията и уменията за изграждане на здрави и мащабируеми Next.js приложения.

Какво е файлово-базирано маршрутизиране?

Файлово-базираното маршрутизиране е система, при която структурата на маршрутите на вашето приложение се определя директно от организацията на вашите файлове и директории. В Next.js App Router вие дефинирате маршрути, като създавате файлове в директорията `app`. Всяка папка представлява сегмент от маршрута, а специални файлове в тези папки дефинират как ще се обработва този сегмент. Този подход предлага няколко предимства:

Първи стъпки с 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`) дефинира потребителски интерфейс, който е споделен между няколко страници в рамките на един сегмент от маршрута. Оформленията са полезни за създаване на последователни хедъри, футъри, странични ленти и други елементи, които трябва да присъстват на няколко страници.

Пример:

Да кажем, че искате да добавите хедър както към страницата `/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` ще бъде заменен с потребителския интерфейс, рендиран от файла `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. Групи маршрути (Route Groups)

Групите маршрути `(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. Прихващащи маршрути

Прихващащите маршрути ви позволяват да заредите маршрут от друга част на вашето приложение в текущото оформление. Това е полезно за създаване на модални прозорци, галерии с изображения и други елементи на потребителския интерфейс, които трябва да се появят върху съществуващото съдържание на страницата. Прихващащите маршрути се дефинират с помощта на синтаксиса `(..)`, който показва колко нива нагоре в дървото на директориите трябва да се отиде, за да се намери прихванатият маршрут.

Пример:

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: 'Продукт А' },
        { 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();
}

// Вижте "Съвпадащи пътища" по-долу, за да научите повече
export const config = {
  matcher: '/admin/:path*',
}

Този пример дефинира middleware, който проверява дали потребителят е удостоверен, преди да му позволи достъп до който и да е маршрут под `/admin`. Ако потребителят не е удостоверен, той се пренасочва към страницата `/login`.

Най-добри практики за файлово-базирано маршрутизиране

За да се възползвате максимално от системата за файлово-базирано маршрутизиране на App Router, вземете предвид следните най-добри практики:

Примери за интернационализация с 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),
  bg: () => import('./dictionaries/bg.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`, `/bg`). Преводите се зареждат динамично въз основа на езиковата версия.

2. Маршрутизиране по домейни

За по-разширен подход можете да използвате различни домейни или поддомейни за всяка езикова версия. Това често включва допълнителна конфигурация с вашия хостинг доставчик.

3. Middleware за откриване на езикова версия

Използвайте middleware за автоматично откриване на предпочитаната езикова версия на потребителя и съответното му пренасочване.

// middleware.js
import { NextResponse } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';

let locales = ['en', 'bg', '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, 'bg'); // Използвайте "bg" като език по подразбиране
  } catch (error) {
      console.error("Грешка при съвпадение на езиковата версия:", error);
      return 'bg'; // Връщане към български, ако съвпадението е неуспешно
  }
}

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 предоставя солидна основа за изграждане на достъпни преживявания, но е от съществено значение да се прилагат най-добрите практики, за да се гарантира, че вашето приложение е използваемо от всички, независимо от техните способности.

Ключови съображения за достъпност

  1. Семантичен HTML: Използвайте семантични HTML елементи (напр. `<article>`, `<nav>`, `<aside>`, `<main>`), за да структурирате съдържанието си. Това придава смисъл на помощните технологии и помага на потребителите да навигират по-лесно в сайта ви.
  2. ARIA атрибути: Използвайте ARIA (Accessible Rich Internet Applications) атрибути, за да подобрите достъпността на персонализирани компоненти и уиджети. ARIA атрибутите предоставят допълнителна информация за ролята, състоянието и свойствата на елементите на помощните технологии.
  3. Навигация с клавиатура: Уверете се, че всички интерактивни елементи са достъпни чрез клавиатура. Потребителите трябва да могат да навигират в приложението ви с помощта на клавиша `Tab` и да взаимодействат с елементи с помощта на клавиша `Enter` или `Space`.
  4. Контраст на цветовете: Използвайте достатъчен цветови контраст между текста и фона, за да осигурите четливост за потребители със зрителни увреждания. Насоките за достъпност на уеб съдържанието (WCAG) препоръчват контрастно съотношение от поне 4.5:1 за нормален текст и 3:1 за голям текст.
  5. Alt текст на изображенията: Осигурете описателен alt текст за всички изображения. Alt текстът предоставя текстова алтернатива за изображенията, която може да бъде прочетена от екранни четци.
  6. Етикети на формуляри: Свържете етикетите на формулярите със съответните им полета за въвеждане, като използвате елемента `<label>`. Това изяснява на потребителите каква информация се очаква във всяко поле.
  7. Тестване с екранен четец: Тествайте приложението си с екранен четец, за да се уверите, че е достъпно за потребители със зрителни увреждания. Популярните екранни четци включват NVDA, JAWS и VoiceOver.

Внедряване на достъпност в Next.js App Router

  1. Използвайте компонента Link на Next.js: Използвайте компонента `<Link>` за навигация. Той предоставя вградени функции за достъпност, като предварително извличане и управление на фокуса.
  2. Управление на фокуса: Когато навигирате между страници или отваряте модални прозорци, уверете се, че фокусът се управлява правилно. Фокусът трябва да бъде зададен на най-логичния елемент на новата страница или модален прозорец.
  3. Достъпни персонализирани компоненти: Когато създавате персонализирани компоненти, уверете се, че те са достъпни, като следвате принципите, описани по-горе. Използвайте семантичен HTML, ARIA атрибути и навигация с клавиатура, за да направите компонентите си използваеми от всички.
  4. Linting и тестване: Използвайте инструменти за линтинг като ESLint с плъгини за достъпност, за да идентифицирате потенциални проблеми с достъпността във вашия код. Също така, използвайте автоматизирани инструменти за тестване, за да тествате приложението си за нарушения на достъпността.

Заключение

Системата за файлово-базирано маршрутизиране на Next.js App Router предлага мощен и интуитивен начин за структуриране и навигация на вашите приложения. Като разберете основните концепции и най-добрите практики, описани в това ръководство, можете да изграждате здрави, мащабируеми и лесни за поддръжка Next.js приложения. Експериментирайте с различните функции на App Router и открийте как той може да опрости работния ви процес и да подобри потребителското изживяване.