javascript

Искусство проектирования URL: Роутинг, Query и Hash параметры

  • пятница, 5 сентября 2025 г. в 00:00:05
https://habr.com/ru/articles/943918/

Привет, Хабр! Меня зовут Алексей Фомин, я Technical Lead во Frontend в компании Devs Universe. В своей работе я часто сталкиваюсь с тем, что даже опытные разработчики не всегда задумываются о проектировании URL-структуры приложения, а ведь это критически важный элемент пользовательского опыта, SEO и архитектуры. В этой статье я хочу системно разобрать анатомию URL и дать практические рекомендации по его проектированию.

Содержание

  1. Анатомия URL: из чего он состоит?

  2. Иерархия и структура (Правильное название URL)

  3. Проектирование роутинга (Маршрутизация)

  4. Query Parameters (Параметры строки запроса)

  5. Hash (Фрагмент)

  6. Сводная таблица: что и когда использовать?

  7. Как получить части URL в JavaScript?

  8. Работа с "неидеальными" Query-параметрами

  9. Чеклист правильного проектирования URL

 

Плохой 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)

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 с кодом, который должен выполниться для обработки запроса.

Подходы:

  1. Серверный роутинг (Traditional): каждый URL ведет на отдельную HTML-страницу, которую генерирует и возвращает сервер. При переходе по ссылке браузер полностью обновляет страницу:

    • Плюсы: проще для SEO (HTML приходит сразу), не требует JavaScript на клиенте;

    • Минусы: медленнее с точки зрения пользователя, больше нагрузки на сервер.

  2. Клиентский роутинг (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.


Query Parameters (Параметры строки запроса)

Это часть 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-документ вернет сервер (в классической модели). Они обрабатываются уже на загруженной странице.


Hash (Фрагмент)

Это часть URL, которая начинается с символа #.
https://example.com/documentation#chapter-2

Когда использовать?

  1. Якорь на странице (Основное назначение): Браузер автоматически прокручивает страницу к элементу с id="chapter-2". Это чисто клиентская навигация, серверу хэш не отправляется.

  2. Клиентский роутинг в старых SPA (History API): Раньше, до появления History API, хэш (# и #!) использовали для организации роутинга в одностраничных приложениях, так как изменение хэша не вызывает перезагрузки страницы и не отправляется на сервер:

    • Пример: example.com/#/dashboard, example.com/#/users;

    • Сейчас это устаревший подход. Современные фреймворки используют History Mode (роуты без #), который создает красивые URL вида example.com/dashboard. Это требует правильной настройки сервера.


Сводная таблица: что и когда использовать?

Компонент URL

Пример

Когда использовать?

Отправляется на сервер?

Путь (Path)

/catalog/watches/racer-x

Для определения ресурса или страницы. Основа SEO и структуры сайта.

Да

Query-параметры

?color=blue&size=42

Для состояния страницы: сортировка, фильтры, поиск, пагинация. Не уникально для страницы.

Да

Хэш (Fragment)

#specifications

Для внутренней навигации по разделам одной страницы. (Устарел для роутинга между страницами в SPA).

Нет


Как получить части URL в JavaScript?

В браузере весь текущий 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-параметрами

На практике вы часто будете сталкиваться с 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 - хэш.

Как это обрабатывает URLSearchParams?

// Представим, что мы на странице с таким 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", Значение: ""

Важные выводы

  1. Пустое значение (?key=) ≠ Отсутствие значения (?key) ≠ Отсутствие ключа:

    • И ?key=, и ?key вернут "" (пустую строку) при вызове .get('key');

    • Но в URL они выглядят по-разному, и некоторые бэкенд-фреймворки могут интерпретировать их неодинаково.

  2. Метод .get() всегда возвращает строку или null. Если значения нет — вернется null. Если значение пустое — вернется пустая строка "".

  3. Для работы с множественными значениями используйте .getAll(). Это особенно важно для чекбоксов, мультиселектов в фильтрах.

Практический совет: всегда явно проверяйте и нормализуйте параметры.

// Вместо этого:
if (urlParams.get('sort')) { 
  // Это не сработает, если sort="", а нам нужно было обработать и этот случай
}

// Делайте так:
const sortValue = urlParams.get('sort');
if (sortValue !== null) {
  // Обрабатываем, даже если sortValue является пустой строкой
  // Это означает, что параметр 'sort' был в URL (пусть и без значения)
}

// Или так:
if (urlParams.has('sort')) {
  // Параметр 'sort' присутствует в URL (даже без значения)
}

Этот подход делает ваш код более надежным и защищенным от нестандартных, но возможных входных данных.


Чеклист правильного проектирования URL

  1. URL читается как путь: /blog/category/article-name;

  2. Используется нижний регистр и дефисы;

  3. Динамические ID скрыты там, где важен смысл: /users/annasmith, а не /users/4815162342;

  4. Query-параметры используются для необязательных настроек (сортировка, фильтры), а не для обязательной информации о странице;

  5. Структура предсказуема: пользователь, посмотрев на URL, может понять, где он и как перейти на главную страницы раздела;

  6. Для SPA настроен History Mode (убирает # из URL), а сервер сконфигурирован корректно (отдавать index.html на все несуществующие пути, которые обрабатывает фронтенд).

Потратив время на проектирование правильной URL-структуры, вы создаете не только удобный и понятный сайт для пользователей, но и закладываете основу для его легкой поддержки и успешного продвижения в поисковых системах.

Также к прочтению:

20 частых антипаттернов в React и как их исправить: кратко, понятно, без мифов