Введение: Почему менять URL без перезагрузки — это суперсила
Представьте: вы открываете современный веб-сервис — скажем, почтовый клиент или онлайн-редактор документов. Вы переключаетесь между папками, открываете письма, меняете настройки — и страница не перезагружается. При этом адресная строка браузера меняется, кнопки «Назад» и «Вперёд» работают, а закладки сохраняют нужное состояние. Как это работает? Магия модификации URL без перезагрузки страницы.
Это не просто «фишка» — это основа производительных одностраничных приложений (SPA). Когда-то разработчики использовали хэши (#), но это было некрасиво и неудобно. Сегодня у нас есть History API — мощный инструмент, который позволяет менять URL мгновенно, без мерцания, без потери состояния, без лишних HTTP-запросов.
В этой статье мы разберём, как работает pushState и replaceState, как они влияют на производительность, и как не наступить на грабли при оптимизации. Поехали!
Основы: History API и его методы
Спецификация HTML5 подарила нам интерфейс window.history. Два главных метода для изменения URL без перезагрузки — pushState и replaceState.
pushState — добавляем новую запись в историю
pushState создаёт новую запись в истории браузера. Пользователь может нажать «Назад» и вернуться к предыдущему URL.
// Синтаксис
history.pushState(state, title, url);
// Пример
history.pushState({ page: 1 }, 'Главная', '/home');
Параметры:
- state — объект данных, связанный с этой записью. Можно хранить любые сериализуемые данные (например, состояние фильтра, ID элемента).
- title — заголовок страницы (большинство браузеров игнорирует, но лучше передавать пустую строку или осмысленный текст).
- url — новый URL. Он должен быть относительным или абсолютным, но в пределах того же origin (протокол + домен + порт).
replaceState — заменяем текущую запись
replaceState изменяет текущую запись в истории, не создавая новой. Пользователь не сможет вернуться «назад» к предыдущему URL — он будет заменён.
// Синтаксис
history.replaceState(state, title, url);
// Пример — обновляем URL при изменении фильтра
function applyFilter(filterName) {
history.replaceState({ filter: filterName }, '', `?filter=${filterName}`);
updateUI(filterName);
}
popstate — слушаем изменения истории
Когда пользователь нажимает «Назад» или «Вперёд», браузер генерирует событие popstate. Важно: pushState и replaceState не вызывают это событие — его нужно обрабатывать отдельно.
window.addEventListener('popstate', function(event) {
if (event.state) {
// Восстанавливаем состояние по данным из event.state
restorePageState(event.state);
} else {
// Если state пустой — возможно, пользователь вышел за пределы нашего приложения
loadDefaultState();
}
});
Оптимизация производительности: что важно знать
Модификация URL — это быстро. Но есть нюансы, которые могут превратить суперсилу в источник багов.
1. Не вызывайте pushState слишком часто
Каждый вызов pushState добавляет запись в историю. Если вы вызываете его на каждое движение мыши или каждый символ в поиске — история раздуется, и пользователь будет нажимать «Назад» 100 раз, чтобы выйти из фильтра. Используйте replaceState для промежуточных состояний, а pushState — для логически завершённых действий (например, открытие новой страницы).
2. Синхронизируйте URL и состояние приложения
Не допускайте рассинхронизации: URL показывает одно, а интерфейс — другое. Это убивает UX. Используйте единый источник истины (например, store в Redux или простой объект состояния), который обновляется вместе с URL.
// Плохо: URL обновляется, а UI — нет
history.pushState({}, '', '/products/123');
// ...а данные не загрузились
// Хорошо: сначала меняем UI, потом URL
async function navigateToProduct(id) {
const product = await fetchProduct(id);
renderProduct(product);
history.pushState({ productId: id }, product.name, `/products/${id}`);
}
3. Используйте replaceState для предотвращения дублирования
Если пользователь несколько раз нажимает одну и ту же кнопку — не плодите записи. Замените текущую.
// Пользователь выбирает категорию "Ноутбуки" — первый раз
history.pushState({ category: 'laptops' }, '', '/category/laptops');
// Он же выбирает ту же категорию снова — заменим, чтобы не дублировать
history.replaceState({ category: 'laptops' }, '', '/category/laptops');
4. Обрабатывайте popstate правильно
Многие забывают, что popstate не даёт вам новый URL напрямую — только объект state. Получить текущий URL можно через location.pathname и location.search. Используйте это для восстановления состояния.
window.addEventListener('popstate', function(event) {
const currentPath = window.location.pathname;
const searchParams = new URLSearchParams(window.location.search);
// Восстанавливаем состояние на основе URL
loadStateFromURL(currentPath, searchParams);
});
Реальный пример: Построение простого SPA-роутера
Давайте соберём маленький, но рабочий пример — роутер для одностраничного приложения. Он будет менять URL, подгружать контент и корректно обрабатывать кнопки «Назад» и «Вперёд».
// Простой SPA-роутер
class Router {
constructor(routes) {
this.routes = routes;
this.currentPath = '';
// Обрабатываем нажатие "Назад"/"Вперёд"
window.addEventListener('popstate', (event) => {
this.handlePopState(event);
});
// Загружаем начальный маршрут
this.loadInitialRoute();
}
// Переход на новый маршрут
navigate(path) {
// Если это уже текущий маршрут — не делаем pushState
if (path === this.currentPath) return;
const route = this.routes[path];
if (route) {
// Обновляем контент
this.renderContent(route);
// Добавляем запись в историю
history.pushState({ path: path }, '', path);
this.currentPath = path;
}
}
// Обработка popstate
handlePopState(event) {
const path = event.state ? event.state.path : window.location.pathname;
const route = this.routes[path];
if (route) {
this.renderContent(route);
this