Перейти к содержанию
Tverknit

History api и hash routing

Когда разработчик начинает новую SPA, встаёт выбор: использовать History API для управления url браузера или hash routing. Это определяет архитектуру навигации в приложении — как браузер обрабатывает историю переходов, как меняется адрес в адресной строке, может ли пользователь вернуться назад через button. Для junior разработчика это выбор между двумя подходами, для senior это trade-off между performance и complexity, для lead это риск дорогих переделок. В этой статье разберём history api и hash routing — оба способа работы с history браузера: как History API с pushstate изменяет url без перезагрузки страницы, как hash routing решает задачу проще для SPA приложений, когда каждый метод работает лучше всего, и как выбор влияет на архитектуру web приложения, react проектов и навигации между страницами.

History api и hash routing — Нейтральный редакционный кадр статьи, общий для анонса и полного текста
Навигация в SPA: History API против hash routing

Как работает History API: от браузера изнутри

История в браузере — это не просто список адресов, которые вы посетили. Это объект Window.history, который вмешивается в адресную строку без перезагрузки страницы. Когда разработчик вызывает pushState, он добавляет новую запись в историю браузера вручную. Ключ в том, что URL меняется в адресной строке, но запрос на сервер не отправляется.

javascript window.history.pushState({ page: 1 }, "Заголовок", "/new-page"); // URL в адресной строке изменится на /new-page // Страница не перезагрузится, но браузер запомнит состояние { page: 1 }

replaceState работает по-другому: она заменяет текущую запись в истории, а не добавляет новую. Это полезно, когда нужно обновить URL без раздувания истории браузера:

javascript window.history.replaceState({ page: 2 }, "Обновлённый", "/updated-page");

Событие popstate и слушание изменений

Когда пользователь нажимает кнопку «Назад» или «Вперёд» браузера, срабатывает событие popstate. Именно здесь слушает JavaScript, чтобы понять, что история изменилась:

javascript window.addEventListener('popstate', function(event) { console.log('История изменилась'); console.log('Состояние:', event.state); // Объект из pushState console.log('Текущий путь:', window.location.pathname);

// Перерисовываем приложение на основе нового URL renderApp(window.location.pathname); });

Это критический момент: popstate срабатывает только при навигации через back/forward, но НЕ на обычных ссылках. Для обработки кликов нужен отдельный обработчик.

Перехват кликов в навигации

В одностраничном приложении обычные ссылки вызовут перезагрузку страницы. Нужно перехватить click события и использовать pushState для смены URL без перезагрузки:

javascript document.addEventListener('click', function(event) { // Проверяем, это наша внутренняя ссылка if (event.target.tagName === 'A' && event.target.dataset.link) { event.preventDefault(); // Отменяем переход по ссылке

const url = event.target.getAttribute('href');
window.history.pushState(null, '', url);
// Рисуем нужную страницу
renderApp(url);

} });

Hash routing: якорь вместо истории

Hash routing работает в совсем другой парадигме. Всё, что после # в URL, остаётся на клиенте и никогда не отправляется на сервер. Это значит, что браузер видит только https://example.com и загружает это с сервера, а всё после # обрабатывается локально в JavaScript.

https://example.com/#/about https://example.com/#/blog/42 https://example.com/#/home

Браузер не пытается загрузить /about с сервера — он знает, что это якорь, локальный элемент страницы. Поэтому hash routing работает везде без настройки сервера.

Слушание hashchange

Когда пользователь меняет hash (кликает по ссылке, нажимает back/forward, обновляет страницу с hash в URL), браузер генерирует событие hashchange:

javascript window.addEventListener('hashchange', function() { console.log('Hash изменился'); console.log('Новый hash:', window.location.hash); // "#/about"

const path = window.location.hash.slice(1); // Убираем символ # renderApp(path); });

Это событие срабатывает при любом изменении hash: при клике по ссылке, нажатии back/forward, вводе URL вручную, обновлении страницы. Это делает hash routing предсказуемым и простым для начинающих разработчиков.

Динамические параметры в URL

С hash routing параметры извлекаются из hash с помощью регулярных выражений:

javascript // URL: https://example.com/#/blog/42 const hash = window.location.hash; // "#/blog/42" const matches = hash.match(/#\/blog\/(\d+)/);

if (matches) { const postId = matches[1]; // "42" loadPost(postId); }

С History API параметры берутся из pathname:

javascript // URL: https://example.com/blog/42 const pathname = window.location.pathname; // "/blog/42" const matches = pathname.match(/\/blog\/(\d+)/);

if (matches) { const postId = matches[1]; // "42" loadPost(postId); }

Простой router на History API

Вот как собрать простой router, который обрабатывает несколько страницы:

javascript const routes = { '/': () => { document.getElementById('app').innerHTML = 'Главная'; }, '/about': () => { document.getElementById('app').innerHTML = 'О нас'; }, '/blog': () => { document.getElementById('app').innerHTML = 'Блог'; }, };

function renderPage(pathname) { const handler = routes[pathname]; if (handler) { handler(); } else { console.log('Страница не найдена'); } }

// Обработка кликов на все ссылки с навигации document.addEventListener('click', function(event) { if (event.target.tagName === 'A' && event.target.dataset.link) { event.preventDefault(); const url = event.target.getAttribute('href'); window.history.pushState(null, '', url); renderPage(url); // Рисуем новую страницу } });

// Обработка back/forward кнопок window.addEventListener('popstate', () => { renderPage(window.location.pathname); });

// Инициализация при загрузке SPA renderPage(window.location.pathname);

Тот же router на hash routing

javascript const routes = { '/': () => { document.getElementById('app').innerHTML = 'Главная'; }, '/about': () => { document.getElementById('app').innerHTML = 'О нас'; }, '/blog': () => { document.getElementById('app').innerHTML = 'Блог'; }, };

function renderPage(hash) { const path = hash.slice(1) || '/'; const handler = routes[path]; if (handler) { handler(); } }

window.addEventListener('hashchange', () => { renderPage(window.location.hash); });

// Инициализация renderPage(window.location.hash);

Почему серверная настройка критична для History API

Когда пользователь открывает URL http://example.com/about прямо в браузере (вручную, через bookmark, при refresh), сервер получает запрос на /about. Если на сервере нет обработки этого пути, вернётся 404 ошибка. Это главное различие между двумя подходами.

Для History API нужно настроить сервер так, чтобы все неизвестные пути возвращали index.html. С Express это выглядит просто:

javascript const express = require('express'); const app = express();

app.use(express.static('public'));

// Любой неизвестный GET запрос вернёт index.html app.get('*', (req, res) => { res.sendFile(__dirname + '/public/index.html'); });

app.listen(3000);

Hash routing не требует таких настроек. Браузер всегда загружает корневой путь, сервер этого не видит и не нуждается в какой-либо конфигурации для маршрутов.

Edge cases: что часто забывают

Refresh страницы: при History API пользователь может обновить страницу на /about и получить 404, если сервер не настроен. С hash routing refresh всегда загружает корневой путь, потом JavaScript парсит hash и отрисовывает нужное содержимое.

Bookmarks: с History API пользователь может создать bookmark на /about, и при переходе браузер загрузит её правильно (при настроенном сервере). С hash routing bookmark выглядит как /#/about — менее красиво для пользователей, но надёжно работает везде.

Back/Forward между приложениями: когда История браузера переключается между вашей SPA и внешними сайтами, History API срабатывает через popstate, hash routing тоже работает, но может быть менее очевидно для пользователей.

Вввести URL вручную: пользователь скопирует адрес из строки и отправит другу. С History API это должно работать при правильной серверной конфигурации. С hash routing работает везде без подготовки.

Нажатие на ссылку без preventDefault: частая ошибка — забыть отменить стандартное поведение ссылки. Браузер загрузит новую страницу несмотря на pushState в коде.

DispatchEvent для popstate: вручную генерировать popstate затратно. Лучше вызвать функцию рендеринга напрямую после pushState.

React и Vue: как они выбирают

React Router и vue-router используют History API как стандартное поведение. Это современный подход. Но обе библиотеки имеют параметры для включения hash mode, если нужна совместимость с очень старыми браузерами или хостингом, который не позволяет переконфигурировать сервер.

История браузера — это не просто деталь реализации, это архитектурный выбор, который влияет на весь проект.

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

Выбор между History API и hash routing

Hash routing подходит для legacy сайтов без доступа к серверной настройке. History API требуется для современного SPA с поддержкой deep links и SEO индексации. SSR приложения обязательно используют History API с fallback на index.html. Выбор зависит от архитектуры проекта и возможностей серверной части хостинга.

Минимальный роутер на ванилла JavaScript

Простой router использует window.location.pathname и addEventListener для popstate события. Функция renderRoute выбирает нужный компонент по текущему пути URL. Const routes хранит объект соответствия path к функциям рендеринга. Клики на ссылки обновляют URL через history.pushState(). Страница обновляется без полной перезагрузки браузером.

Серверная настройка для History API

Браузер отправляет запрос на /blog/123, но сервер возвращает 404 без fallback. Решение: настроить редирект всех неизвестных путей на index.html. Express требует catch-all роута после статических файлов. Nginx использует директиву try_files для location. React Router и Vue Router работают корректно после правильной настройки серверной конфигурации.

Наверх