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: от браузера изнутри
История в браузере — это не просто список адресов, которые вы посетили. Это объект 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, если нужна совместимость с очень старыми браузерами или хостингом, который не позволяет переконфигурировать сервер.
История браузера — это не просто деталь реализации, это архитектурный выбор, который влияет на весь проект.