Искусство проектирования URL: Роутинг, Query и Hash параметры
- пятница, 5 сентября 2025 г. в 00:00:05
Привет, Хабр! Меня зовут Алексей Фомин, я Technical Lead во Frontend в компании Devs Universe. В своей работе я часто сталкиваюсь с тем, что даже опытные разработчики не всегда задумываются о проектировании URL-структуры приложения, а ведь это критически важный элемент пользовательского опыта, SEO и архитектуры. В этой статье я хочу системно разобрать анатомию URL и дать практические рекомендации по его проектированию.
Содержание
Плохой URL — это как кривая вывеска на магазине: он путает пользователей, усложняет навигацию и портит впечатление о всем сайте. Хороший URL, наоборот, понятен, предсказуем и несет смысловую нагрузку.
Прежде чем проектировать, давайте разберемся, как URL устроен. Возьмем пример:
https://www.example.com:8080/catalog/search?q=watch&sort=price#product-list
Часть URL | Пример | Описание |
Протокол | https: | Правила обмена данными (http, https, ftp) |
Доменное имя | www.example.com | Адрес сервера. www — поддомен, example — доменное имя, .com — домен верхнего уровня (TLD) |
Порт | :8080 | "Дверь" на сервере. По умолчанию для HTTP — 80, для HTTPS — 443 (в URL обычно не отображается) |
Путь (Path) | /catalog/search | Иерархический путь к конкретному ресурсу на сервере. Основа для роутинга |
Query-параметры | ?q=watch&sort=price | Начинаются с ?. Пара ключ=значение, разделенные &. Нужны для параметров страницы (фильтры, поиск, сортировка) |
Хэш (Фрагмент) | #product-list | Начинается с #. Якорь внутри страницы. Браузер автоматически прокрутит к элементу с id="product-list". На сервер не отправляется |
URL должен отражать структуру ваших данных, быть читаемым и логичным.
Человеко-понятность (Readable): используйте слова, а не ID там, где это уместно:
Плохо: /p/12345
;
Хорошо: /products/modern-wristwatch
;
Отлично: /catalog/watches/wrist/modern-wristwatch
(показывает иерархию).
Иерархичность (Hierarchical): стройте путь от общего к частному, как путь в файловой системе:
/{section}/{category}/{subcategory}/{item}
;
Пример: /blog/javascript/frameworks/vue-3-composition-api
— сразу понятно, где мы находимся.
Множественное число: часто используют множественное число для ресурсов-коллекций:
/users/
(список пользователей), /users/annasmith
(конкретный пользователь);
Это не строгое правило, но распространенная конвенция в RESTful API.
Лаконичность: не усложняйте путь без необходимости. Избегайте длинных предложений:
Плохо: /site.com/blog/posts/article-about-how-to-build-a-website-in-2024/
;
Хорошо: /blog/website-development-2024
.
Постоянство (Persistence): URL — это обещание. Once published, forever available. Не меняйте URL существующих страниц без настройки редиректа со старого адреса на новый (301 Moved Permanently). Иначе вы потеряете пользователей и SEO-вес.
Регистр букв: единообразие! Чаще всего используют нижний регистр. Сервера могут быть чувствительны к регистру (Website.com/PAGE
и website.com/page
могут вести в разные места), что приводит к ошибкам 404.
Разделители: для разделения слов в составе пути используйте дефисы (-
):
Плохо: modern_wristwatch
(подчеркивание плохо выделяется в подчеркнутых ссылках);
Плохо: modern%20wristwatch
(пробел, виден как %20);
Хорошо: modern-wristwatch
(дефис, читается легко и дружелюбен для SEO).
Роутинг — это механизм, который сопоставляет URL с кодом, который должен выполниться для обработки запроса.
Серверный роутинг (Traditional): каждый URL ведет на отдельную HTML-страницу, которую генерирует и возвращает сервер. При переходе по ссылке браузер полностью обновляет страницу:
Плюсы: проще для SEO (HTML приходит сразу), не требует JavaScript на клиенте;
Минусы: медленнее с точки зрения пользователя, больше нагрузки на сервер.
Клиентский роутинг (SPA - Single Page Application): сервер отдает один HTML-файл, а JavaScript на стороне клиента (например, React Router, Vue Router) управляет URL и подгружает нужные "страницы" (на самом деле, компоненты) динамически, без полной перезагрузки:
Плюсы: очень быстрое переключение между "страницами", поведение как у нативного приложения;
Минусы: сложнее с SEO (изначально решается с помощью SSR - Server-Side Rendering), первоначальная загрузка может быть дольше.
Статические пути: /about
, /contact
.
Динамические параметры: Используются для идентификации конкретного ресурса:
Путь: /users/:userId
или /products/:productId
;
Пример: URL /users/annasmith
-> параметр userId = "annasmith"
.
Вложенные роуты (Nested Routes): Отражают иерархию в UI:
Роут: /settings/:tab
(e.g., /settings/profile
, /settings/security
);
Роут: /dashboard/analytics/overview
.
Это часть URL, которая начинается с ?
и состоит из пар ключ=значение
, разделенных &
.
https://example.com/products?category=watches&sort=price\_asc&page=2
Фильтрация, сортировка, поиск:
?category=electronics&price_min=100&price_max=500
;
?sort=date_desc
(сортировка по дате, по убыванию);
?q=search+query
(поисковый запрос).
Пагинация: ?page=3&limit=25
.
Настройки представления: ?view=grid
или ?view=list
.
Отслеживание (UTM-метки): ?utm_source=newsletter&utm_medium=email&utm_campaign=promo
.
Сохранение состояния, которое не должно быть в пути: Параметры запроса не уникальны для страницы. Страница /products
с разными query-параметрами — это все та же страница /products
, просто в разном состоянии.
Важно: Query-параметры не влияют на то, какой HTML-документ вернет сервер (в классической модели). Они обрабатываются уже на загруженной странице.
Это часть URL, которая начинается с символа #
.
https://example.com/documentation#chapter-2
Якорь на странице (Основное назначение): Браузер автоматически прокручивает страницу к элементу с id="chapter-2"
. Это чисто клиентская навигация, серверу хэш не отправляется.
Клиентский роутинг в старых SPA (History API): Раньше, до появления History API, хэш (#
и #!
) использовали для организации роутинга в одностраничных приложениях, так как изменение хэша не вызывает перезагрузки страницы и не отправляется на сервер:
Пример: example.com/#/dashboard
, example.com/#/users
;
Сейчас это устаревший подход. Современные фреймворки используют History Mode (роуты без #
), который создает красивые URL вида example.com/dashboard
. Это требует правильной настройки сервера.
Компонент URL | Пример | Когда использовать? | Отправляется на сервер? |
Путь (Path) |
| Для определения ресурса или страницы. Основа SEO и структуры сайта. | Да |
Query-параметры |
| Для состояния страницы: сортировка, фильтры, поиск, пагинация. Не уникально для страницы. | Да |
Хэш (Fragment) |
| Для внутренней навигации по разделам одной страницы. (Устарел для роутинга между страницами в SPA). | Нет |
В браузере весь текущий URL доступен в глобальных объектах window.location
или просто location
. Этот объект содержит все части URL в разобранном виде.
Пример URL для разбора:
https://www.example.com:8080/catalog/search?q=watch&sort=price#product-list
// Объект location для нашего примера
console.log(location);
// Выведет:
// {
// href: "https://www.example.com:8080/catalog/search?q=watch&sort=price#product-list",
// protocol: "https:",
// hostname: "www.example.com",
// port: "8080",
// host: "www.example.com:8080", // hostname + port
// pathname: "/catalog/search",
// search: "?q=watch&sort=price",
// hash: "#product-list"
// }
// 1. Получить весь URL
const fullUrl = location.href;
// 2. Получить путь (Path)
const path = location.pathname; // Вернёт: "/catalog/search"
// 3. Получить строку query-параметров
const searchString = location.search; // Вернёт: "?q=watch&sort=price"
// 4. Получить хэш
const hash = location.hash; // Вернёт: "#product-list"
// 5. Работа с Query-параметрами
// Простой способ получить значение конкретного параметра
const urlParams = new URLSearchParams(location.search);
const searchQuery = urlParams.get('q'); // Вернёт: "watch"
const sortType = urlParams.get('sort'); // Вернёт: "price"
const nonExistentParam = urlParams.get('page'); // Вернёт: null
// Перебрать все параметры
for (let [key, value] of urlParams) {
console.log(`${key}: ${value}`);
}
// Выведет:
// q: watch
// sort: price
// 6. Динамическое создание URL с параметрами
// Полезно для формирования ссылок с query-параметрами
const newParams = new URLSearchParams();
newParams.append('category', 'electronics');
newParams.append('page', '2');
const newUrl = `${location.origin}/search?${newParams.toString()}`;
// newUrl будет: "https://www.example.com:8080/search?category=electronics&page=2"
На практике вы часто будете сталкиваться с query-строками, которые не соответствуют идеальному шаблону ключ=значение
. JavaScript-методы должны корректно обрабатывать эти случаи.
Рассмотрим пример URL с проблемными параметрами:
https://example.com/search?q=watch&sort=&page=2&filter=size&filter=color&flag&&invalid\_value=#test
Разберем его части:
q=watch
- нормальный параметр;
sort=
- ключ есть, значение пустое;
page=2
- нормальный параметр;
filter=size&filter=color
- два параметра с одинаковым ключом;
flag
- ключ без знака равенства и значения;
&invalid_value=
- значение без ключа (редко, но бывает);
#test
- хэш.
// Представим, что мы на странице с таким URL
const urlParams = new URLSearchParams('?q=watch&sort=&page=2&filter=size&filter=color&flag&&invalid_value=');
// 1. Нормальный параметр
console.log(urlParams.get('q')); // "watch"
// 2. Параметр с пустым значением
console.log(urlParams.get('sort')); // "" (пустая строка, не null!)
// 3. Несколько параметров с одинаковым ключом
// .get() возвращает только ПЕРВОЕ значение
console.log(urlParams.get('filter')); // "size"
// Чтобы получить все значения, нужно использовать .getAll()
console.log(urlParams.getAll('filter')); // ["size", "color"]
// 4. Ключ без значения и без знака "=" (flag)
// Интерпретируется как параметр с пустым значением
console.log(urlParams.get('flag')); // "" (пустая строка)
// 5. Значение без ключа (invalid_value=)
// Интерпретируется как параметр с ключом "invalid_value" и пустым значением
console.log(urlParams.get('invalid_value')); // ""
// 6. Проверка существования ключа
// .has() проверяет наличие ключа, даже если значение пустое
console.log(urlParams.has('sort')); // true
console.log(urlParams.has('flag')); // true
console.log(urlParams.has('nonexistent')); // false
// 7. Перебор всех параметров
for (let [key, value] of urlParams) {
console.log(`Ключ: "${key}", Значение: "${value}"`);
}
// Выведет:
// Ключ: "q", Значение: "watch"
// Ключ: "sort", Значение: ""
// Ключ: "page", Значение: "2"
// Ключ: "filter", Значение: "size"
// Ключ: "filter", Значение: "color"
// Ключ: "flag", Значение: ""
// Ключ: "invalid_value", Значение: ""
Пустое значение (?key=
) ≠ Отсутствие значения (?key
) ≠ Отсутствие ключа:
И ?key=
, и ?key
вернут ""
(пустую строку) при вызове .get('key')
;
Но в URL они выглядят по-разному, и некоторые бэкенд-фреймворки могут интерпретировать их неодинаково.
Метод .get()
всегда возвращает строку или null
. Если значения нет — вернется null
. Если значение пустое — вернется пустая строка ""
.
Для работы с множественными значениями используйте .getAll()
. Это особенно важно для чекбоксов, мультиселектов в фильтрах.
Практический совет: всегда явно проверяйте и нормализуйте параметры.
// Вместо этого:
if (urlParams.get('sort')) {
// Это не сработает, если sort="", а нам нужно было обработать и этот случай
}
// Делайте так:
const sortValue = urlParams.get('sort');
if (sortValue !== null) {
// Обрабатываем, даже если sortValue является пустой строкой
// Это означает, что параметр 'sort' был в URL (пусть и без значения)
}
// Или так:
if (urlParams.has('sort')) {
// Параметр 'sort' присутствует в URL (даже без значения)
}
Этот подход делает ваш код более надежным и защищенным от нестандартных, но возможных входных данных.
URL читается как путь: /blog/category/article-name
;
Используется нижний регистр и дефисы;
Динамические ID скрыты там, где важен смысл: /users/annasmith
, а не /users/4815162342
;
Query-параметры используются для необязательных настроек (сортировка, фильтры), а не для обязательной информации о странице;
Структура предсказуема: пользователь, посмотрев на URL, может понять, где он и как перейти на главную страницы раздела;
Для SPA настроен History Mode (убирает #
из URL), а сервер сконфигурирован корректно (отдавать index.html
на все несуществующие пути, которые обрабатывает фронтенд).
Потратив время на проектирование правильной URL-структуры, вы создаете не только удобный и понятный сайт для пользователей, но и закладываете основу для его легкой поддержки и успешного продвижения в поисковых системах.
20 частых антипаттернов в React и как их исправить: кратко, понятно, без мифов