Главная / Статьи / Единый источник правды для маршрутов в приложениях Next.js

Единый источник правды для маршрутов в приложениях Next.js

Узнайте, как модуль централизованного маршрутизирования заменяет разрозненные строковые пути в Next.js с использованием вспомогательных функций для динамических сегментов и группировки экспортов маршрутов.

1222 слов

Большинство проектов Next.js начинаются с ввода путей непосредственно в компоненты Link и вызовы router.push. Это быстро для первых нескольких страниц, но по мере роста приложения одни и те же строки дублируются в навигационных панелях, футерах, перенаправлениях и электронных письмах, и каждая замена названия превращается в сложную задачу по поиску и замене. Проблема усугубляется, когда помощники для написания кода на основе ИИ генерируют часть кода, поскольку они редко последовательно соблюдают единые правила. Решением является перемещение всех путей в один модуль с заданными значениями.

В чем проблема с встроенными путями

Вот типичный компонент навигации, где каждая цель представляет собой литеральную строку:

// ❌ Problematic approach

import Link from 'next/link'
import { useRouter } from 'next/router'

export default function Navbar() {
  const router = useRouter()

  const handleNavigation = () => {
    router.push('/dashboard/user/profile')
  }

  return (
    <nav>
      <Link href="/about">About</Link>
      <Link href="/contact">Contact</Link>
      <Link href="/blog">Blog</Link>
      <Link href="/dashboard/analytics">Analytics</Link>
      <button onClick={handleNavigation}>Profile</button>
    </nav>
  )
}

Сейчас здесь всё работает нормально, но по мере роста кодовой базы и команды возникают четыре проблемы.

Замена названий превращается в сложную задачу

Замена /blog на /articles подразумевает поиск всех вхождений этого пути в компонентах, серверном коде, тестах и конфигурации. Если что-то упустить, останется нерабочая ссылка.

Ошибки написания остаются незамеченными

Путь вроде /dashbord — это просто строка, поэтому ни компилятор, ни инструменты проверки кода её не обрабатывают. Ошибка обычно проявляется в виде ошибки 404, иногда только в продакшене.

Редактор не может помочь

Поскольку маршруты представляют собой неструктурированные строки, отсутствует автодополнение, возможность «перейти к определению» и предупреждения о том, что маршрут больше не существует.

Члены команды расходятся во мнениях

Разные люди и разные запросы к ИИ приводят к тому, что один и тот же путь записывается немного по-разному: с или без заканчивающейся слешом, с вручную составленной строкой запроса и так далее.

Централизация маршрутов в одном модуле

Решение заключается в единственном файле конфигурации, который импортируется всеми частями приложения.

Где находится файл

Папка constants или config, расположенная рядом с папками app и components, облегчает его поиск:

/src
  /app
  /components
  /constants
    └── routes.ts
  /lib

Определение объекта маршрутов

Приведённый ниже модуль группирует маршруты по категориям. Статические страницы представляют собой простые строки. Динамические страницы — это небольшие функции, которые принимают переменную с участком пути и возвращают полный путь, так что вызывающие код не вынуждены вручную собирать URL-адреса. Концовки API находятся в вложенной ключевой строке API для использования в вызовах функции fetch. Добавление as const позволяет TypeScript определить точные литеральные значения и делает объект только для чтения, что обеспечивает точное автодополнение и предотвращает случайную переопределение.

// constants/routes.ts
export const ROUTES = {
  // Public routes
  HOME: '/',
  ABOUT: '/about',
  CONTACT: '/contact',
  PRICING: '/pricing',

  // Auth routes
  LOGIN: '/auth/login',
  REGISTER: '/auth/register',
  FORGOT_PASSWORD: '/auth/forgot-password',

  // Blog routes
  BLOG: '/blog',
  BLOG_DETAIL: (slug: string) => `/blog/${slug}`,
  BLOG_CATEGORY: (category: string) => `/blog/category/${category}`,

  // Dashboard routes
  DASHBOARD: '/dashboard',
  DASHBOARD_ANALYTICS: '/dashboard/analytics',
  DASHBOARD_SETTINGS: '/dashboard/settings',
  DASHBOARD_PROFILE: '/dashboard/profile',

  // Dynamic user routes
  USER_PROFILE: (userId: string) => `/user/${userId}`,
  USER_POSTS: (userId: string) => `/user/${userId}/posts`,

  // API routes (useful for fetch calls)
  API: {
    USERS: '/api/users',
    POSTS: '/api/posts',
    AUTH: '/api/auth',
  },
} as const

// Export route groups for better organization
export const AUTH_ROUTES = {
  LOGIN: ROUTES.LOGIN,
  REGISTER: ROUTES.REGISTER,
  FORGOT_PASSWORD: ROUTES.FORGOT_PASSWORD,
} as const

export const DASHBOARD_ROUTES = {
  HOME: ROUTES.DASHBOARD,
  ANALYTICS: ROUTES.DASHBOARD_ANALYTICS,
  SETTINGS: ROUTES.DASHBOARD_SETTINGS,
  PROFILE: ROUTES.DASHBOARD_PROFILE,
} as const

Два дополнительных экспорта, AUTH_ROUTES и DASHBOARD_ROUTES, представляют собой вьюы основного объекта, предназначенные для кода, интересующегося только одной областью, например, мидлвэра, защищающего панель управления. Поскольку они ссылаются на ROUTES, а не повторяют строки, по-прежнему существует лишь одно определение на каждый путь.

Одна улучшение, которое стоит рассмотреть: если слаг или ID могут содержать пробелы или специальные символы, необходимо обернуть их в encodeURIComponent внутри вспомогательной функции, чтобы каждый вызывающий получал безопасный URL автоматически.

Использование маршрутов в компонентах

Теперь navbar импортирует имена вместо строк. Как Link, так и императивное навигирование читают данные из одного и того же модуля, поэтому между ними никогда не может возникнуть разногласий:

// components/Navbar.tsx
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ROUTES, DASHBOARD_ROUTES } from '@/constants/routes'

export default function Navbar() {
  const router = useRouter()

  const handleProfileNavigation = () => {
    router.push(DASHBOARD_ROUTES.PROFILE)
  }

  return (
    <nav className="flex space-x-4">
      <Link href={ROUTES.ABOUT} className="hover:text-blue-500">
        About
      </Link>
      <Link href={ROUTES.CONTACT} className="hover:text-blue-500">
        Contact
      </Link>
      <Link href={ROUTES.BLOG} className="hover:text-blue-500">
        Blog
      </Link>
      <Link href={DASHBOARD_ROUTES.ANALYTICS} className="hover:text-blue-500">
        Analytics
      </Link>
      <button
        onClick={handleProfileNavigation}
        className="rounded bg-blue-500 px-4 py-2 text-white"
      >
        Profile
      </button>
    </nav>
  )
}

Обратите внимание, что в этом примере импортируется функция useRouter из модуля next/router, который используется в роутере Pages. В проектах с App Router её следует импортировать из next/navigation внутри клиентского компонента; модуль routes работает одинаково в обоих случаях.

Создание динамических ссылок

Динамические сегменты — это та область, где такой подход приносит наибольшую пользу. В приведённом ниже списке каждая запись и её категория ссылаются посредством вызова вспомогательных функций с данными из соответствующей записи:

// components/BlogList.tsx
import Link from 'next/link'
import { ROUTES } from '@/constants/routes'

interface Post {
  slug: string
  title: string
  category: string
}

interface BlogListProps {
  posts: Post[]
}

export default function BlogList({ posts }: BlogListProps) {
  return (
    <div className="space-y-4">
      {posts.map((post) => (
        <article key={post.slug} className="rounded border p-4">
          <h2 className="text-xl font-bold">
            <Link href={ROUTES.BLOG_DETAIL(post.slug)} className="hover:text-blue-500">
              {post.title}
            </Link>
          </h2>
          <Link
            href={ROUTES.BLOG_CATEGORY(post.category)}
            className="text-sm text-gray-500 hover:text-blue-500"
          >
            Category: {post.category}
          </Link>
        </article>
      ))}
    </div>
  )
}

Если структура URL для записей когда-либо изменится, например, будет добавлен сегмент с годом, достаточно обновить BLOG_DETAIL один раз, и все вызовы автоматически соответствуют новой структуре. TypeScript также выдаст предупреждение при вызове с неправильным количеством аргументов.

Преимущества

  • Каждый путь определяется один раз и используется повсюду.
  • Неправильно написанные названия маршрутов приводят к ошибкам во время компиляции, а не к выводу сообщений 404.
  • Изменение URL требует лишь одной правки.
  • Структура остается упорядоченной по мере увеличения количества страниц.
  • Редакторы предоставляют автодополнение и проверку ввода для каждого пункта назначения.
  • Одни и те же константы используются для навигационных полос, футеров, ситемапов и трекинг-линков.
  • Один файл позволяет быстро ознакомиться со всеми страницами приложения.
  • Ограничения и альтернативы

    Файл констант не проверяет, действительно ли существует соответствующая страница в каталогах app или pages. Если папка переименовывается без обновления модуля, ссылки всё равно перестают работать. Next.js также предлагает опцию типизированных маршрутов, которая генерирует типы на основе файловой системы; ознакомьтесь с актуальной документацией, чтобы узнать её статус и подходит ли она вашей конфигурации. Многие команды используют оба подхода: генерируемые типы для обеспечения корректности и модуль констант для удобства чтения и использования вспомогательных функций.

    Когда ИИ-ассистенты добавляют код, укажите им в инструкциях к проекту импортировать элементы из модуля маршрутов.

    Заключение

    Рассматривайте URL как часть API вашего приложения, а не просто разрозненные строки. Типизированный, централизованный модуль маршрутов позволяет изменять названия папок путём одной правки, выявлять ошибки в редакторе и обеспечивает как людям, так и инструментам одно очевидное место для поиска информации.