События popstate

Событие popstate является частью API History и возникает при изменении текущей записи истории браузера. Оно срабатывает, когда пользователь перемещается по истории с помощью кнопок «назад» и «вперёд», а также при программном вызове методов history.back(), history.forward() и history.go().

Ключевая особенность: событие не инициируется при вызове history.pushState() или history.replaceState(). Оно возникает только при переходе между уже существующими записями истории.

window.addEventListener('popstate', (event) => {
    console.log('Сработало событие popstate');
    console.log(event.state);
});

Объект события содержит свойство state, которое представляет собой данные, ранее переданные в pushState или replaceState.


Взаимодействие с History API

Методы History API позволяют управлять историей браузера без перезагрузки страницы:

  • history.pushState(state, title, url) — добавляет новую запись
  • history.replaceState(state, title, url) — заменяет текущую запись
  • history.back() — переход назад
  • history.forward() — переход вперёд

Пример добавления состояния:

history.pushState({ page: 'about' }, '', '/about');

При возврате к этой записи через кнопку «назад» произойдёт событие popstate, и в event.state будет:

{ page: 'about' }

Особенности поведения popstate

1. Различия между браузерами

В некоторых браузерах (особенно старых версиях) событие popstate может срабатывать при загрузке страницы. В современных реализациях оно не вызывается при первом открытии страницы.

2. Асинхронная природа

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

3. Отсутствие срабатывания при pushState

history.pushState({ page: 'home' }, '', '/home');
// popstate НЕ вызовется

Это важно учитывать при разработке маршрутизаторов.


Роль popstate в Page.js

Библиотека Page.js реализует клиентскую маршрутизацию, используя History API. Событие popstate является основным механизмом отслеживания навигации пользователя.

Основной принцип

Page.js подписывается на popstate и при его срабатывании:

  1. Анализирует текущий URL
  2. Сопоставляет его с зарегистрированными маршрутами
  3. Вызывает соответствующий обработчик

Упрощённая схема:

window.addEventListener('popstate', function () {
    page.dispatch(location.pathname);
});

Обработка навигации назад и вперёд

При использовании Page.js пользовательская навигация через браузер автоматически обрабатывается:

page('/home', () => {
    console.log('Главная страница');
});

page('/about', () => {
    console.log('О нас');
});

page();

Последовательность:

  1. Переход на /home
  2. Переход на /about
  3. Нажатие «назад»
  4. Срабатывает popstate
  5. Page.js вызывает обработчик /home

Работа с event.state

Хотя Page.js в основном ориентируется на URL, доступ к event.state остаётся важным для хранения дополнительной информации.

window.addEventListener('popstate', (event) => {
    if (event.state) {
        console.log('Данные состояния:', event.state);
    }
});

Использование:

history.pushState({ scrollY: window.scrollY }, '', '/page');

При возврате можно восстановить позицию прокрутки:

window.addEventListener('popstate', (event) => {
    if (event.state && event.state.scrollY) {
        window.scrollTo(0, event.state.scrollY);
    }
});

Синхронизация состояния приложения

Одной из задач popstate является восстановление состояния интерфейса при навигации.

Пример:

page('/products/:id', (ctx) => {
    loadProduct(ctx.params.id);
});

При возврате назад:

  • URL меняется
  • popstate срабатывает
  • Page.js повторно вызывает обработчик
  • интерфейс синхронизируется

Частые ошибки при работе с popstate

Игнорирование различий между pushState и popstate

Ожидание, что pushState вызовет popstate:

history.pushState({}, '', '/new');
// обработчик popstate НЕ выполнится

Дублирование логики

Иногда обработка маршрута выполняется и при pushState, и при popstate, что приводит к дублированию:

function navigate(path) {
    history.pushState({}, '', path);
    render(path); // вручную
}

И отдельно:

window.addEventListener('popstate', () => {
    render(location.pathname);
});

Page.js решает эту проблему централизованной маршрутизацией.


Управление прокруткой

Браузеры по умолчанию могут сохранять позицию прокрутки, но при SPA-навигации это поведение часто нужно контролировать вручную.

window.addEventListener('popstate', () => {
    window.scrollTo(0, 0);
});

Или с использованием сохранённого состояния:

history.pushState({ scroll: window.scrollY }, '');

Интеграция с Page.js middleware

Page.js поддерживает middleware, которые также участвуют в обработке popstate.

page((ctx, next) => {
    console.log('Переход:', ctx.path);
    next();
});

При срабатывании popstate:

  1. Page.js получает новый путь
  2. Запускает цепочку middleware
  3. Выполняет обработчик маршрута

Поведение при прямом изменении URL

Если пользователь вручную изменяет URL:

  • страница перезагружается
  • popstate не участвует

Если изменение происходит через историю:

  • popstate срабатывает
  • Page.js перехватывает управление

Комбинирование с hash-навигацией

popstate не реагирует на изменения hash, если не используется History API.

Для hash:

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

Page.js может работать в режиме hash, но тогда используется другой механизм.


Отладка событий popstate

Полезный шаблон:

window.addEventListener('popstate', (event) => {
    console.log({
        pathname: location.pathname,
        state: event.state
    });
});

Позволяет отслеживать:

  • текущий URL
  • состояние истории
  • последовательность переходов

Взаимодействие с сервером

При использовании History API сервер должен корректно обрабатывать маршруты:

  • любой URL должен возвращать HTML-приложение
  • маршрутизация выполняется на клиенте

Иначе при обновлении страницы возникнет ошибка 404.


Практическая схема работы Page.js с popstate

  1. Пользователь кликает по ссылке
  2. Page.js вызывает pushState
  3. Обработчик маршрута выполняется
  4. Пользователь нажимает «назад»
  5. Браузер активирует предыдущую запись
  6. Срабатывает popstate
  7. Page.js анализирует URL
  8. Вызывается соответствующий обработчик

Минимальная реализация маршрутизатора на основе popstate

const routes = {
    '/': () => console.log('Главная'),
    '/about': () => console.log('О нас')
};

function render(path) {
    if (routes[path]) {
        routes[path]();
    }
}

window.addEventListener('popstate', () => {
    render(location.pathname);
});

function navigate(path) {
    history.pushState({}, '', path);
    render(path);
}

Этот пример иллюстрирует ту же концепцию, которую Page.js реализует более полноценно.


Контроль истории

Можно ограничивать поведение пользователя:

window.addEventListener('popstate', (event) => {
    if (!event.state) {
        history.pushState({ blocked: true }, '', location.href);
    }
});

Однако такие подходы требуют осторожности и могут ухудшать пользовательский опыт.


Производительность

Событие popstate само по себе лёгкое, но обработчики могут быть тяжёлыми:

  • повторная отрисовка интерфейса
  • загрузка данных
  • обновление состояния

Оптимизация включает:

  • кеширование данных
  • ленивую загрузку
  • минимизацию DOM-операций

Безопасность и ограничения

  • нельзя изменить домен через pushState
  • URL должен быть в рамках текущего origin
  • данные state не должны содержать чувствительную информацию

Связь с жизненным циклом SPA

popstate — один из ключевых элементов SPA:

  • обеспечивает навигацию без перезагрузки
  • синхронизирует URL и состояние приложения
  • позволяет использовать стандартные элементы браузера (кнопки назад/вперёд)

Page.js использует это событие как основу своей архитектуры маршрутизации, обеспечивая предсказуемое и управляемое поведение навигации.