Единый источник правды для маршрутов в приложениях Next.js
Узнайте, как модуль централизованного маршрутизирования заменяет разрозненные строковые пути в Next.js с использованием вспомогательных функций для динамических сегментов и группировки экспортов маршрутов.
Большинство проектов 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 также выдаст предупреждение при вызове с неправильным количеством аргументов.
Преимущества
- Каждый путь определяется один раз и используется повсюду.
Ограничения и альтернативы
Файл констант не проверяет, действительно ли существует соответствующая страница в каталогах app или pages. Если папка переименовывается без обновления модуля, ссылки всё равно перестают работать. Next.js также предлагает опцию типизированных маршрутов, которая генерирует типы на основе файловой системы; ознакомьтесь с актуальной документацией, чтобы узнать её статус и подходит ли она вашей конфигурации. Многие команды используют оба подхода: генерируемые типы для обеспечения корректности и модуль констант для удобства чтения и использования вспомогательных функций.
Когда ИИ-ассистенты добавляют код, укажите им в инструкциях к проекту импортировать элементы из модуля маршрутов.
Заключение
Рассматривайте URL как часть API вашего приложения, а не просто разрозненные строки. Типизированный, централизованный модуль маршрутов позволяет изменять названия папок путём одной правки, выявлять ошибки в редакторе и обеспечивает как людям, так и инструментам одно очевидное место для поиска информации.